Formation API

Version 2. Read and change your organization's schedule, results, rosters, attendance, availability, announcements, strategies and recruitment board from your own tools. OpenAPI file

Getting started

An admin creates a key under Settings → Integrations → API keys, choosing exactly what it can read and change. Keys start with fmtn_v2_ and are shown once. Send yours on every request:

curl https://formation.esports-tools.com/api/v2/schedule \
  -H "Authorization: Bearer fmtn_v2_…"

Start with GET /api/v2: it shows which organization the key belongs to and its scopes. Changes made with a key are recorded in the audit log as the person who created it, marked as coming from the API. Keep keys on a server — never in a website or app people can download.

Scopes

Each endpoint needs one scope. A key without it gets a 403 that names the missing scope.

ScopeAllows
org:readName, timezone, roles, venues, event types and event detail fields.
org:writeAdd, edit and remove event detail fields.
teams:readTeams and who's on each roster, with in-game names, positions and ranks.
teams:writeEdit roster entries: in-game name, position, number, rank, starter.
members:readEveryone in the organization: name, role, teams and when they joined.
members:emailAlso include members' email addresses.
schedule:readMatches, practices, scrims and events, with results, event details and lineups.
schedule:writeSchedule, edit, cancel and delete matches and events, record results and set lineups.
attendance:readWho's in, who's out and who hasn't answered, for every match and event.
attendance:writeMark someone in, out, attended, absent or late.
availability:readMembers' weekly availability and their time off.
opponents:readOpponents, with their contact details, notes and scouting reports (screenshots included).
opponents:writeAdd and edit opponents.
announcements:readPublished and scheduled announcements.
announcements:writePost announcements (they notify members and post to Discord like any other).
strategies:readEvery team's strategies and playbook notes.
strategies:writeAdd, edit and delete strategies.
recruitment:readProspects, including their contact details, and recruitment levels.
recruitment:writeAdd prospects, edit them and move them between stages.
stats:readTeam records, win rates and head-to-head results.
webhooks:readThe webhooks this key registered, and their recent deliveries.
webhooks:writeRegister webhooks so Formation calls the tool when things change — only for events the key can already read.

Requests & responses

Responses are JSON. One thing comes back as { "data": { … } }; lists as { "data": [ … ] }. Every object has an id and an object type, and a url to open it in Formation where there is one.

Pages. Long lists come a page at a time (limit, up to 100) with "pagination": { "nextCursor": "…", "hasMore": true }. Pass cursor=nextCursor for the next page until hasMore is false.

Times come back in UTC (ISO 8601) with the event's own timezone alongside. When you send one, include an offset (2026-10-24T19:00:00-05:00) for an exact moment, or leave it off (2026-10-24T19:00) to mean that time in the organization's timezone.

Filters that take several values accept them comma-separated (teamId=a,b). Use updatedSince to sync only what changed.

Writes. POST creates, PATCH changes only the fields you send (send null to clear one), PUT sets a result or attendance, DELETE removes. Scheduling, results and announcements notify people and post to Discord just as they do in Formation; add "notify": false when creating to skip that.

Safe retries. Send an Idempotency-Key header on a POST (any string unique to that request — a UUID works well) and a retry with the same key, within 24 hours, returns the first response instead of doing it twice. The response comes back with Idempotent-Replayed: true. Reusing a key with a different body is a 422; a new attempt needs a new key.

Event details — the maps, lobby codes and other fields from Settings → Event Details — come back as details. Set them with an object keyed by field label or id: { "details": { "Maps": ["Ascent", "Bind"] } }.

Limits. Each key can make 120 requests a minute. Every response carries X-RateLimit-Remaining and X-RateLimit-Reset.

Errors look like { "error": { "code", "message", "details", "requestId" } }:

400invalid_requestA query parameter or the body isn't valid JSON or isn't allowed.
401unauthorized / key_expiredNo key, a wrong or revoked key, or an expired one.
403missing_scopeThe key doesn't have the scope this needs — details.scope names it.
404not_foundNo such thing in your organization.
422validation_failedThe body was understood but a value isn't allowed — details lists each field.
429rate_limitedToo many requests; wait for Retry-After seconds.

Verifying webhooks

Every delivery carries a Formation-Signature header: t=<unix seconds>,v1=<hex HMAC-SHA256>. The signature is computed over <t>.<raw request body>, keyed by the secret you were shown when you registered the webhook (under Webhooks below) — compute it over the exact bytes you received, before parsing JSON. Formation-Event names the event type and Formation-Delivery is a stable id for that delivery's retries, so you can drop a duplicate if your endpoint sees it twice.

import crypto from "node:crypto";

function isValid(rawBody: string, header: string, secret: string) {
  const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300; // reject anything older than 5 minutes
  return fresh && crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
}

A delivery that fails (a non-2xx response, a timeout, or no response) is retried with backoff for about a day; a webhook that keeps failing is turned off, and disabledReason on the webhook says why. Use Send test or POST .../test to check your endpoint before relying on it.

Key & organization

Check a key works, and read the organization's setup.

GET/api/v2

Check your key

The organization and scopes this key has. Works with any valid key.

Any valid key

Example response (200)
{
  "data": {
    "object": "api_key_info",
    "apiVersion": "2",
    "organization": {
      "id": "cmorg_nova",
      "name": "Nova Esports",
      "slug": "nova-esports"
    },
    "key": {
      "id": "cmkey_1",
      "name": "Stats dashboard",
      "scopes": [
        "schedule:read",
        "teams:read"
      ]
    },
    "rateLimitPerMinute": 120,
    "docs": "https://formation.example/developers",
    "openapi": "https://formation.example/api/v2/openapi.json"
  }
}
GET/api/v2/organization

Get the organization

Scope org:read

Example response (200)
{
  "data": {
    "id": "cmorg_nova",
    "object": "organization",
    "name": "Nova Esports",
    "slug": "nova-esports",
    "timezone": "America/Chicago",
    "logoUrl": null,
    "websiteUrl": "https://novaesports.gg",
    "themeColor": "#EA580C",
    "tracksAttendance": {
      "practices": true,
      "scrims": true,
      "matches": true
    },
    "counts": {
      "teams": 2,
      "members": 13
    },
    "createdAt": "2026-01-12T17:04:00.000Z"
  }
}
GET/api/v2/roles

List roles

Scope org:read

Example response (200)
{
  "data": [
    {
      "id": "cmrole_cap",
      "object": "role",
      "name": "Captain",
      "description": null,
      "color": "#F59E0B",
      "isSystem": true
    }
  ]
}
GET/api/v2/venues

List venues

Scope org:read

Example response (200)
{
  "data": [
    {
      "id": "cmven_1",
      "object": "venue",
      "name": "Campus Esports Arena",
      "isOnline": false,
      "onlineUrl": null,
      "address": {
        "line1": "100 University Ave",
        "line2": null,
        "city": "Missoula",
        "state": "MT",
        "postalCode": "59812",
        "country": "USA"
      },
      "capacity": 40,
      "contact": {
        "name": "Front desk",
        "phone": null,
        "email": null
      },
      "notes": null,
      "timezone": "America/Denver",
      "createdAt": "2026-02-01T15:00:00.000Z",
      "updatedAt": "2026-02-01T15:00:00.000Z"
    }
  ]
}
GET/api/v2/venues/{venueId}

Get a venue

Scope org:read

Example response (200)
{
  "data": {
    "id": "cmven_1",
    "object": "venue",
    "name": "Campus Esports Arena",
    "isOnline": false,
    "onlineUrl": null,
    "address": {
      "line1": "100 University Ave",
      "line2": null,
      "city": "Missoula",
      "state": "MT",
      "postalCode": "59812",
      "country": "USA"
    },
    "capacity": 40,
    "contact": {
      "name": "Front desk",
      "phone": null,
      "email": null
    },
    "notes": null,
    "timezone": "America/Denver",
    "createdAt": "2026-02-01T15:00:00.000Z",
    "updatedAt": "2026-02-01T15:00:00.000Z"
  }
}
GET/api/v2/event-types

List custom event types

Used as eventTypeId when creating a type EVENT.

Scope org:read

Example response (200)
{
  "data": [
    {
      "id": "cmet_1",
      "object": "event_type",
      "name": "Community night",
      "tracksAttendance": true,
      "createdAt": "2026-03-01T15:00:00.000Z"
    }
  ]
}
GET/api/v2/event-details

List event detail fields

The extra details your organization fills in when scheduling (Settings → Event Details). Their ids or labels are the keys of `details` when you create or edit a match or event.

Scope org:read

Example response (200)
{
  "data": [
    {
      "id": "cmfld_maps",
      "object": "event_detail_field",
      "label": "Maps",
      "type": "MULTI_SELECT",
      "options": [
        "Ascent",
        "Bind",
        "Haven",
        "Lotus"
      ],
      "appliesTo": [
        "SCRIM",
        "MATCH",
        "PRACTICE"
      ],
      "game": "Valorant",
      "required": false,
      "showInDiscord": true
    }
  ]
}
POST/api/v2/event-details

Add an event detail field

Sets up a new field, the same as Settings → Event Details. A label that's already taken comes back as 409 with the existing field's id in `details.fieldId`.

Scope org:write

Body (JSON)

FieldTypeNotes
label *string
type *TEXT | LONG_TEXT | SELECT | MULTI_SELECT | LINK
optionsstring[]Choices for SELECT and MULTI_SELECT; ignored for the other types.Default: []
appliesTo *MATCH | SCRIM | PRACTICE | EVENT[]Which kinds of event ask for it.
gamestring | nullOnly for teams playing this game (as written on the team). Leave out for every team.
requiredbooleanRequired details must be filled in when scheduling in Formation.Default: false
showInDiscordbooleanInclude it in Discord reminders and new-event posts.Default: true
Example response (201)
{
  "data": {
    "id": "cmfld_maps",
    "object": "event_detail_field",
    "label": "Maps",
    "type": "MULTI_SELECT",
    "options": [
      "Ascent",
      "Bind",
      "Haven",
      "Lotus"
    ],
    "appliesTo": [
      "SCRIM",
      "MATCH",
      "PRACTICE"
    ],
    "game": "Valorant",
    "required": false,
    "showInDiscord": true
  }
}
GET/api/v2/event-details/{fieldId}

Get an event detail field

Scope org:read

Example response (200)
{
  "data": {
    "id": "cmfld_maps",
    "object": "event_detail_field",
    "label": "Maps",
    "type": "MULTI_SELECT",
    "options": [
      "Ascent",
      "Bind",
      "Haven",
      "Lotus"
    ],
    "appliesTo": [
      "SCRIM",
      "MATCH",
      "PRACTICE"
    ],
    "game": "Valorant",
    "required": false,
    "showInDiscord": true
  }
}
PATCH/api/v2/event-details/{fieldId}

Edit an event detail field

Scope org:write

Body (JSON)

FieldTypeNotes
labelstring
typeTEXT | LONG_TEXT | SELECT | MULTI_SELECT | LINK
optionsstring[]
appliesToMATCH | SCRIM | PRACTICE | EVENT[]
gamestring | nullOnly for teams playing this game (as written on the team). Leave out for every team.
requiredboolean
showInDiscordboolean
Example response (200)
{
  "data": {
    "id": "cmfld_maps",
    "object": "event_detail_field",
    "label": "Maps",
    "type": "MULTI_SELECT",
    "options": [
      "Ascent",
      "Bind",
      "Haven",
      "Lotus"
    ],
    "appliesTo": [
      "SCRIM",
      "MATCH",
      "PRACTICE"
    ],
    "game": "Valorant",
    "required": false,
    "showInDiscord": true
  }
}
DELETE/api/v2/event-details/{fieldId}

Remove an event detail field

Values already saved on events stay in their JSON but stop showing anywhere.

Scope org:write

Returns 204 with no body.

Teams & rosters

Teams, who's on them, and their records.

GET/api/v2/teams

List teams

Scope teams:read

Query

  • game (string) — Only teams playing this game.
  • include (roster) — roster adds each team's roster.
Example response (200)
{
  "data": [
    {
      "id": "cmteam_val",
      "name": "Nova Valorant",
      "game": "Valorant",
      "object": "team",
      "slug": "nova-valorant",
      "logoUrl": null,
      "rosterSize": 7,
      "url": "https://formation.example/nova-esports/teams/nova-valorant",
      "createdAt": "2026-01-12T17:10:00.000Z",
      "updatedAt": "2026-09-02T20:41:00.000Z"
    }
  ]
}
GET/api/v2/teams/{teamId}

Get a team

Scope teams:read

Query

  • include (roster) — roster adds the team's roster.
Example response (200)
{
  "data": {
    "id": "cmteam_val",
    "name": "Nova Valorant",
    "game": "Valorant",
    "object": "team",
    "slug": "nova-valorant",
    "logoUrl": null,
    "rosterSize": 7,
    "url": "https://formation.example/nova-esports/teams/nova-valorant",
    "createdAt": "2026-01-12T17:10:00.000Z",
    "updatedAt": "2026-09-02T20:41:00.000Z"
  }
}
GET/api/v2/teams/{teamId}/roster

List a team's roster

Scope teams:read

Query

  • starters (boolean) — true for starters only, false for subs only.
Example response (200)
{
  "data": [
    {
      "object": "roster_entry",
      "membershipId": "cmmem_ava",
      "teamId": "cmteam_val",
      "name": "Ava Nguyen",
      "role": {
        "id": "cmrole_player",
        "name": "Player"
      },
      "inGameName": "ava#NA1",
      "position": "Duelist",
      "jerseyNumber": "7",
      "rank": "Immortal 2",
      "isStarter": true,
      "bio": null,
      "trackers": {
        "valorant": "https://tracker.gg/valorant/profile/riot/ava%23NA1",
        "rocketLeague": null,
        "smash": null,
        "leagueOfLegends": null,
        "other": null
      },
      "joinedTeamAt": "2026-01-15T00:00:00.000Z"
    }
  ]
}
GET/api/v2/teams/{teamId}/roster/{membershipId}

Get a roster entry

Scope teams:read

Example response (200)
{
  "data": {
    "object": "roster_entry",
    "membershipId": "cmmem_ava",
    "teamId": "cmteam_val",
    "name": "Ava Nguyen",
    "role": {
      "id": "cmrole_player",
      "name": "Player"
    },
    "inGameName": "ava#NA1",
    "position": "Duelist",
    "jerseyNumber": "7",
    "rank": "Immortal 2",
    "isStarter": true,
    "bio": null,
    "trackers": {
      "valorant": "https://tracker.gg/valorant/profile/riot/ava%23NA1",
      "rocketLeague": null,
      "smash": null,
      "leagueOfLegends": null,
      "other": null
    },
    "joinedTeamAt": "2026-01-15T00:00:00.000Z"
  }
}
PATCH/api/v2/teams/{teamId}/roster/{membershipId}

Edit a roster entry

In-game name, position, number, rank, starter, bio and tracker links. Adding or removing players is done in Formation.

Scope teams:write

Body (JSON)

FieldTypeNotes
inGameNamestring | null
positionstring | null
jerseyNumberstring | null
rankstring | null
isStarterboolean
biostring | null
trackersobject
Example response (200)
{
  "data": {
    "object": "roster_entry",
    "membershipId": "cmmem_ava",
    "teamId": "cmteam_val",
    "name": "Ava Nguyen",
    "role": {
      "id": "cmrole_player",
      "name": "Player"
    },
    "inGameName": "ava#NA1",
    "position": "Duelist",
    "jerseyNumber": "7",
    "rank": "Immortal 2",
    "isStarter": true,
    "bio": null,
    "trackers": {
      "valorant": "https://tracker.gg/valorant/profile/riot/ava%23NA1",
      "rocketLeague": null,
      "smash": null,
      "leagueOfLegends": null,
      "other": null
    },
    "joinedTeamAt": "2026-01-15T00:00:00.000Z"
  }
}
GET/api/v2/teams/{teamId}/stats

Get a team's record

Match and scrim records, recent results and head-to-head by opponent. from/to limit which results count.

Scope stats:read

Query

  • from (date-time) — Only items scheduled at or after this time.
  • to (date-time) — Only items scheduled before this time.
Example response (200)
{
  "data": {
    "object": "team_stats",
    "team": {
      "id": "cmteam_val",
      "name": "Nova Valorant",
      "game": "Valorant"
    },
    "range": {
      "from": null,
      "to": null
    },
    "matches": {
      "played": 12,
      "wins": 8,
      "losses": 3,
      "draws": 1,
      "winRate": 0.667,
      "recent": [
        {
          "outcome": "WIN",
          "at": "2026-10-10T00:00:00.000Z",
          "opponent": {
            "id": "cmopp_apex",
            "name": "Apex Legionnaires"
          },
          "scoreFor": 2,
          "scoreAgainst": 1
        }
      ]
    },
    "scrims": {
      "played": 20,
      "wins": 11,
      "losses": 9,
      "draws": 0,
      "winRate": 0.55,
      "recent": []
    },
    "upcomingMatches": 2,
    "headToHead": [
      {
        "opponent": {
          "id": "cmopp_apex",
          "name": "Apex Legionnaires"
        },
        "matches": {
          "played": 2,
          "wins": 1,
          "losses": 1,
          "draws": 0,
          "winRate": 0.5
        },
        "scrims": {
          "played": 4,
          "wins": 3,
          "losses": 1,
          "draws": 0,
          "winRate": 0.75
        }
      }
    ]
  }
}

Members & availability

Everyone in the organization, and when they can play. Private member info (Settings → Member Info) isn't available through the API.

GET/api/v2/members

List members

Emails are included only for keys with members:email.

Scope members:read

Query

  • teamId (comma-separated) — One or more team ids (comma-separated).
  • roleId (comma-separated) — One or more role ids.
  • status (ACTIVE | SUSPENDED) — Membership status.
  • q (string) — Search by name (and email, with members:email).
  • limit (integer) — Items per page, 1–100. Default 25.
  • cursor (string) — The nextCursor from the previous page.
Example response (200)
{
  "data": [
    {
      "id": "cmmem_ava",
      "object": "member",
      "name": "Ava Nguyen",
      "email": "[email protected]",
      "role": {
        "id": "cmrole_player",
        "name": "Player"
      },
      "status": "ACTIVE",
      "discordLinked": true,
      "teams": [
        {
          "id": "cmteam_val",
          "name": "Nova Valorant",
          "game": "Valorant",
          "inGameName": "ava#NA1",
          "position": "Duelist",
          "isStarter": true
        }
      ],
      "joinedAt": "2026-01-15T00:00:00.000Z"
    }
  ],
  "pagination": {
    "limit": 25,
    "nextCursor": null,
    "hasMore": false
  }
}
GET/api/v2/members/{membershipId}

Get a member

Scope members:read

Example response (200)
{
  "data": {
    "id": "cmmem_ava",
    "object": "member",
    "name": "Ava Nguyen",
    "email": "[email protected]",
    "role": {
      "id": "cmrole_player",
      "name": "Player"
    },
    "status": "ACTIVE",
    "discordLinked": true,
    "teams": [
      {
        "id": "cmteam_val",
        "name": "Nova Valorant",
        "game": "Valorant",
        "inGameName": "ava#NA1",
        "position": "Duelist",
        "isStarter": true
      }
    ],
    "joinedAt": "2026-01-15T00:00:00.000Z"
  }
}
GET/api/v2/availability

List availability

Each member's weekly availability (in their own timezone, dayOfWeek 0 = Sunday) and exceptions between from and to (default: the next 60 days).

Scope availability:read

Query

  • teamId (comma-separated) — One or more team ids (comma-separated).
  • membershipId (comma-separated) — One or more member ids.
  • from (date-time) — Only items scheduled at or after this time.
  • to (date-time) — Only items scheduled before this time.
  • limit (integer) — Items per page, 1–100. Default 25.
  • cursor (string) — The nextCursor from the previous page.
Example response (200)
{
  "data": [
    {
      "object": "availability",
      "membershipId": "cmmem_ava",
      "name": "Ava Nguyen",
      "ignoresAvailability": false,
      "weekly": [
        {
          "dayOfWeek": 1,
          "startTime": "18:00",
          "endTime": "22:00",
          "timezone": "America/Chicago",
          "teamId": null,
          "effectiveFrom": null,
          "effectiveUntil": null
        }
      ],
      "exceptions": [
        {
          "date": "2026-10-30T00:00:00.000Z",
          "startTime": null,
          "endTime": null,
          "available": false,
          "reason": "Out of town"
        }
      ]
    }
  ],
  "pagination": {
    "limit": 25,
    "nextCursor": null,
    "hasMore": false
  }
}

Schedule

Matches live at /matches; practices, scrims and custom events at /events. Creating one notifies the people involved and posts to Discord the same way scheduling in Formation does — send "notify": false to skip that.

GET/api/v2/schedule

Everything in a date range

Matches and events together, in time order — one call for a calendar. from defaults to now and to to 30 days later; a range covers at most 366 days.

Scope schedule:read

Query

  • from (date-time) — Only items scheduled at or after this time.
  • to (date-time) — Only items scheduled before this time.
  • teamId (comma-separated) — One or more team ids (comma-separated).
Example response (200)
{
  "data": [
    {
      "id": "cmmatch_1",
      "object": "match",
      "team": {
        "id": "cmteam_val",
        "name": "Nova Valorant",
        "game": "Valorant"
      },
      "opponent": {
        "id": "cmopp_apex",
        "name": "Apex Legionnaires"
      },
      "scheduledAt": "2026-10-24T00:00:00.000Z",
      "timezone": "America/Chicago",
      "format": "BO3",
      "location": {
        "type": "ONLINE",
        "venue": null
      },
      "stream": {
        "platform": "Twitch",
        "url": "https://twitch.tv/novaesports",
        "caster": null
      },
      "status": "SCHEDULED",
      "result": null,
      "notes": null,
      "details": [
        {
          "fieldId": "cmfld_maps",
          "label": "Maps",
          "type": "MULTI_SELECT",
          "value": [
            "Ascent",
            "Bind",
            "Haven"
          ]
        }
      ],
      "url": "https://formation.example/nova-esports/schedule/matches/cmmatch_1",
      "createdAt": "2026-10-20T18:02:00.000Z",
      "updatedAt": "2026-10-20T18:02:00.000Z"
    }
  ]
}
GET/api/v2/matches

List matches

Scope schedule:read

Query

  • from (date-time) — Only items scheduled at or after this time.
  • to (date-time) — Only items scheduled before this time.
  • updatedSince (date-time) — Only items changed at or after this time — for syncing.
  • teamId (comma-separated) — One or more team ids (comma-separated).
  • opponentId (comma-separated) — One or more opponent ids.
  • status (SCHEDULED | LIVE | COMPLETED | CANCELLED | FORFEIT) — Match status.
  • outcome (WIN | LOSS | DRAW) — Result.
  • order (asc | desc) — By scheduled time. Default asc.
  • include (attendance) — attendance adds each attendance list (needs attendance:read).
  • limit (integer) — Items per page, 1–100. Default 25.
  • cursor (string) — The nextCursor from the previous page.
Example response (200)
{
  "data": [
    {
      "id": "cmmatch_1",
      "object": "match",
      "team": {
        "id": "cmteam_val",
        "name": "Nova Valorant",
        "game": "Valorant"
      },
      "opponent": {
        "id": "cmopp_apex",
        "name": "Apex Legionnaires"
      },
      "scheduledAt": "2026-10-24T00:00:00.000Z",
      "timezone": "America/Chicago",
      "format": "BO3",
      "location": {
        "type": "ONLINE",
        "venue": null
      },
      "stream": {
        "platform": "Twitch",
        "url": "https://twitch.tv/novaesports",
        "caster": null
      },
      "status": "SCHEDULED",
      "result": null,
      "notes": null,
      "details": [
        {
          "fieldId": "cmfld_maps",
          "label": "Maps",
          "type": "MULTI_SELECT",
          "value": [
            "Ascent",
            "Bind",
            "Haven"
          ]
        }
      ],
      "url": "https://formation.example/nova-esports/schedule/matches/cmmatch_1",
      "createdAt": "2026-10-20T18:02:00.000Z",
      "updatedAt": "2026-10-20T18:02:00.000Z"
    }
  ],
  "pagination": {
    "limit": 25,
    "nextCursor": null,
    "hasMore": false
  }
}
POST/api/v2/matches

Schedule a match

Scope schedule:write

Body (JSON)

FieldTypeNotes
teamId *string
opponentIdstringAn existing opponent. Give this or opponentName.
opponentNamestringCreates a new opponent with this name. Give this or opponentId.
scheduledAt *stringISO 8601 date-time. With an offset or Z (2026-10-24T19:00:00-05:00) it's that exact moment; without one (2026-10-24T19:00) it's read in the organization's timezone.
formatBO1 | BO3 | BO5 | BO7 | OTHERDefault: "BO1"
locationTypeONLINE | LANDefault: "ONLINE"
venueIdstring | nullRequired when locationType is LAN.
isStreamedbooleanDefault: false
streamPlatformstring | null
streamUrlurl | null
casterNamestring | null
notesstring | null
detailsobjectEvent details keyed by field id or label (any case), e.g. { "Maps": ["Ascent", "Bind"], "Lobby code": "NOVA-4471" }. Replaces every detail on the event. See GET /event-details.
notifybooleanNotify the people involved and post to Discord, the same as scheduling in Formation does.Default: true
Example response (201)
{
  "data": {
    "id": "cmmatch_1",
    "object": "match",
    "team": {
      "id": "cmteam_val",
      "name": "Nova Valorant",
      "game": "Valorant"
    },
    "opponent": {
      "id": "cmopp_apex",
      "name": "Apex Legionnaires"
    },
    "scheduledAt": "2026-10-24T00:00:00.000Z",
    "timezone": "America/Chicago",
    "format": "BO3",
    "location": {
      "type": "ONLINE",
      "venue": null
    },
    "stream": {
      "platform": "Twitch",
      "url": "https://twitch.tv/novaesports",
      "caster": null
    },
    "status": "SCHEDULED",
    "result": null,
    "notes": null,
    "details": [
      {
        "fieldId": "cmfld_maps",
        "label": "Maps",
        "type": "MULTI_SELECT",
        "value": [
          "Ascent",
          "Bind",
          "Haven"
        ]
      }
    ],
    "url": "https://formation.example/nova-esports/schedule/matches/cmmatch_1",
    "createdAt": "2026-10-20T18:02:00.000Z",
    "updatedAt": "2026-10-20T18:02:00.000Z"
  }
}
GET/api/v2/matches/{matchId}

Get a match

Scope schedule:read

Query

  • include (attendance) — attendance adds each attendance list (needs attendance:read).
Example response (200)
{
  "data": {
    "id": "cmmatch_1",
    "object": "match",
    "team": {
      "id": "cmteam_val",
      "name": "Nova Valorant",
      "game": "Valorant"
    },
    "opponent": {
      "id": "cmopp_apex",
      "name": "Apex Legionnaires"
    },
    "scheduledAt": "2026-10-24T00:00:00.000Z",
    "timezone": "America/Chicago",
    "format": "BO3",
    "location": {
      "type": "ONLINE",
      "venue": null
    },
    "stream": {
      "platform": "Twitch",
      "url": "https://twitch.tv/novaesports",
      "caster": null
    },
    "status": "SCHEDULED",
    "result": null,
    "notes": null,
    "details": [
      {
        "fieldId": "cmfld_maps",
        "label": "Maps",
        "type": "MULTI_SELECT",
        "value": [
          "Ascent",
          "Bind",
          "Haven"
        ]
      }
    ],
    "url": "https://formation.example/nova-esports/schedule/matches/cmmatch_1",
    "createdAt": "2026-10-20T18:02:00.000Z",
    "updatedAt": "2026-10-20T18:02:00.000Z"
  }
}
PATCH/api/v2/matches/{matchId}

Edit a match

A new scheduledAt restarts the match's reminders.

Scope schedule:write

Body (JSON)

FieldTypeNotes
opponentIdstringAn existing opponent. Give this or opponentName.
opponentNamestringCreates a new opponent with this name. Give this or opponentId.
scheduledAtstringISO 8601 date-time. With an offset or Z (2026-10-24T19:00:00-05:00) it's that exact moment; without one (2026-10-24T19:00) it's read in the organization's timezone.
formatBO1 | BO3 | BO5 | BO7 | OTHER
locationTypeONLINE | LAN
venueIdstring | nullRequired when locationType is LAN.
isStreamedboolean
streamPlatformstring | null
streamUrlurl | null
casterNamestring | null
notesstring | null
detailsobjectEvent details keyed by field id or label (any case), e.g. { "Maps": ["Ascent", "Bind"], "Lobby code": "NOVA-4471" }. Replaces every detail on the event. See GET /event-details.
Example response (200)
{
  "data": {
    "id": "cmmatch_1",
    "object": "match",
    "team": {
      "id": "cmteam_val",
      "name": "Nova Valorant",
      "game": "Valorant"
    },
    "opponent": {
      "id": "cmopp_apex",
      "name": "Apex Legionnaires"
    },
    "scheduledAt": "2026-10-24T00:00:00.000Z",
    "timezone": "America/Chicago",
    "format": "BO3",
    "location": {
      "type": "ONLINE",
      "venue": null
    },
    "stream": {
      "platform": "Twitch",
      "url": "https://twitch.tv/novaesports",
      "caster": null
    },
    "status": "SCHEDULED",
    "result": null,
    "notes": null,
    "details": [
      {
        "fieldId": "cmfld_maps",
        "label": "Maps",
        "type": "MULTI_SELECT",
        "value": [
          "Ascent",
          "Bind",
          "Haven"
        ]
      }
    ],
    "url": "https://formation.example/nova-esports/schedule/matches/cmmatch_1",
    "createdAt": "2026-10-20T18:02:00.000Z",
    "updatedAt": "2026-10-20T18:02:00.000Z"
  }
}
PUT/api/v2/matches/{matchId}/result

Record a result

Also how a match is marked live, cancelled or forfeited. A newly decided result posts to Discord.

Scope schedule:write

Body (JSON)

FieldTypeNotes
status *SCHEDULED | LIVE | COMPLETED | CANCELLED | FORFEIT
outcomeWIN | LOSS | DRAW | nullWho won, for a completed match.
scoreForinteger | null
scoreAgainstinteger | null
Example response (200)
{
  "data": {
    "id": "cmmatch_1",
    "object": "match",
    "team": {
      "id": "cmteam_val",
      "name": "Nova Valorant",
      "game": "Valorant"
    },
    "opponent": {
      "id": "cmopp_apex",
      "name": "Apex Legionnaires"
    },
    "scheduledAt": "2026-10-24T00:00:00.000Z",
    "timezone": "America/Chicago",
    "format": "BO3",
    "location": {
      "type": "ONLINE",
      "venue": null
    },
    "stream": {
      "platform": "Twitch",
      "url": "https://twitch.tv/novaesports",
      "caster": null
    },
    "status": "SCHEDULED",
    "result": null,
    "notes": null,
    "details": [
      {
        "fieldId": "cmfld_maps",
        "label": "Maps",
        "type": "MULTI_SELECT",
        "value": [
          "Ascent",
          "Bind",
          "Haven"
        ]
      }
    ],
    "url": "https://formation.example/nova-esports/schedule/matches/cmmatch_1",
    "createdAt": "2026-10-20T18:02:00.000Z",
    "updatedAt": "2026-10-20T18:02:00.000Z"
  }
}
PATCH/api/v2/matches/{matchId}/details

Change one or more event details

Unlike PATCH on the match itself, this only touches the details you name — set `null` to clear one, and leave the rest exactly as they are. Built for an integration that owns a single detail, like a VOD link, without having to read and resend everything else.

Scope schedule:write

Body (JSON)

FieldTypeNotes
details *objectOnly the details to change, keyed by field id or label (any case). A value sets that detail; null clears it. Details you leave out stay as they are.
Example response (200)
{
  "data": {
    "id": "cmmatch_1",
    "object": "match",
    "team": {
      "id": "cmteam_val",
      "name": "Nova Valorant",
      "game": "Valorant"
    },
    "opponent": {
      "id": "cmopp_apex",
      "name": "Apex Legionnaires"
    },
    "scheduledAt": "2026-10-24T00:00:00.000Z",
    "timezone": "America/Chicago",
    "format": "BO3",
    "location": {
      "type": "ONLINE",
      "venue": null
    },
    "stream": {
      "platform": "Twitch",
      "url": "https://twitch.tv/novaesports",
      "caster": null
    },
    "status": "SCHEDULED",
    "result": null,
    "notes": null,
    "details": [
      {
        "fieldId": "cmfld_maps",
        "label": "Maps",
        "type": "MULTI_SELECT",
        "value": [
          "Ascent",
          "Bind",
          "Haven"
        ]
      }
    ],
    "url": "https://formation.example/nova-esports/schedule/matches/cmmatch_1",
    "createdAt": "2026-10-20T18:02:00.000Z",
    "updatedAt": "2026-10-20T18:02:00.000Z"
  }
}
DELETE/api/v2/matches/{matchId}

Delete a match

Scope schedule:write

Returns 204 with no body.

GET/api/v2/events

List practices, scrims and events

Events for chosen people or roles (instead of a team) have team null and an audience.

Scope schedule:read

Query

  • from (date-time) — Only items scheduled at or after this time.
  • to (date-time) — Only items scheduled before this time.
  • updatedSince (date-time) — Only items changed at or after this time — for syncing.
  • teamId (comma-separated) — One or more team ids (comma-separated).
  • type (PRACTICE | SCRIM | EVENT) — Kind of event.
  • eventTypeId (comma-separated) — One or more custom event types.
  • opponentId (comma-separated) — Scrims against these opponents.
  • order (asc | desc) — By scheduled time. Default asc.
  • include (attendance) — attendance adds each attendance list (needs attendance:read).
  • limit (integer) — Items per page, 1–100. Default 25.
  • cursor (string) — The nextCursor from the previous page.
Example response (200)
{
  "data": [
    {
      "id": "cmsess_1",
      "object": "event",
      "type": "SCRIM",
      "eventType": null,
      "team": {
        "id": "cmteam_val",
        "name": "Nova Valorant",
        "game": "Valorant"
      },
      "audience": null,
      "opponent": {
        "id": "cmopp_apex",
        "name": "Apex Legionnaires"
      },
      "scheduledAt": "2026-10-23T00:00:00.000Z",
      "endsAt": "2026-10-23T01:30:00.000Z",
      "durationMinutes": 90,
      "timezone": "America/Chicago",
      "location": {
        "type": "ONLINE",
        "venue": null
      },
      "result": {
        "outcome": "WIN",
        "scoreFor": 13,
        "scoreAgainst": 9
      },
      "notes": null,
      "details": [
        {
          "fieldId": "cmfld_lobby",
          "label": "Lobby code",
          "type": "TEXT",
          "value": "NOVA-4471"
        }
      ],
      "url": "https://formation.example/nova-esports/schedule/practice/cmsess_1",
      "createdAt": "2026-10-20T18:05:00.000Z",
      "updatedAt": "2026-10-23T02:01:00.000Z"
    }
  ],
  "pagination": {
    "limit": 25,
    "nextCursor": null,
    "hasMore": false
  }
}
POST/api/v2/events

Schedule a practice, scrim or event

Scope schedule:write

Body (JSON)

FieldTypeNotes
teamId *string
type *PRACTICE | SCRIM | EVENT
opponentIdstringScrims only: an existing opponent. Give this or opponentName.
opponentNamestringScrims only: creates a new opponent with this name.
eventTypeIdstringRequired for type EVENT. See GET /event-types.
scheduledAt *stringISO 8601 date-time. With an offset or Z (2026-10-24T19:00:00-05:00) it's that exact moment; without one (2026-10-24T19:00) it's read in the organization's timezone.
durationMinutesintegerDefault: 60
locationTypeONLINE | LANDefault: "ONLINE"
venueIdstring | nullRequired when locationType is LAN.
notesstring | null
detailsobjectEvent details keyed by field id or label (any case), e.g. { "Maps": ["Ascent", "Bind"], "Lobby code": "NOVA-4471" }. Replaces every detail on the event. See GET /event-details.
notifybooleanNotify the people involved and post to Discord, the same as scheduling in Formation does.Default: true
Example response (201)
{
  "data": {
    "id": "cmsess_1",
    "object": "event",
    "type": "SCRIM",
    "eventType": null,
    "team": {
      "id": "cmteam_val",
      "name": "Nova Valorant",
      "game": "Valorant"
    },
    "audience": null,
    "opponent": {
      "id": "cmopp_apex",
      "name": "Apex Legionnaires"
    },
    "scheduledAt": "2026-10-23T00:00:00.000Z",
    "endsAt": "2026-10-23T01:30:00.000Z",
    "durationMinutes": 90,
    "timezone": "America/Chicago",
    "location": {
      "type": "ONLINE",
      "venue": null
    },
    "result": {
      "outcome": "WIN",
      "scoreFor": 13,
      "scoreAgainst": 9
    },
    "notes": null,
    "details": [
      {
        "fieldId": "cmfld_lobby",
        "label": "Lobby code",
        "type": "TEXT",
        "value": "NOVA-4471"
      }
    ],
    "url": "https://formation.example/nova-esports/schedule/practice/cmsess_1",
    "createdAt": "2026-10-20T18:05:00.000Z",
    "updatedAt": "2026-10-23T02:01:00.000Z"
  }
}
GET/api/v2/events/{eventId}

Get an event

Scope schedule:read

Query

  • include (attendance) — attendance adds each attendance list (needs attendance:read).
Example response (200)
{
  "data": {
    "id": "cmsess_1",
    "object": "event",
    "type": "SCRIM",
    "eventType": null,
    "team": {
      "id": "cmteam_val",
      "name": "Nova Valorant",
      "game": "Valorant"
    },
    "audience": null,
    "opponent": {
      "id": "cmopp_apex",
      "name": "Apex Legionnaires"
    },
    "scheduledAt": "2026-10-23T00:00:00.000Z",
    "endsAt": "2026-10-23T01:30:00.000Z",
    "durationMinutes": 90,
    "timezone": "America/Chicago",
    "location": {
      "type": "ONLINE",
      "venue": null
    },
    "result": {
      "outcome": "WIN",
      "scoreFor": 13,
      "scoreAgainst": 9
    },
    "notes": null,
    "details": [
      {
        "fieldId": "cmfld_lobby",
        "label": "Lobby code",
        "type": "TEXT",
        "value": "NOVA-4471"
      }
    ],
    "url": "https://formation.example/nova-esports/schedule/practice/cmsess_1",
    "createdAt": "2026-10-20T18:05:00.000Z",
    "updatedAt": "2026-10-23T02:01:00.000Z"
  }
}
PATCH/api/v2/events/{eventId}

Edit an event

An event's type and team can't change. A new scheduledAt restarts its reminders.

Scope schedule:write

Body (JSON)

FieldTypeNotes
opponentIdstringScrims only: an existing opponent. Give this or opponentName.
opponentNamestringScrims only: creates a new opponent with this name.
eventTypeIdstringRequired for type EVENT. See GET /event-types.
scheduledAtstringISO 8601 date-time. With an offset or Z (2026-10-24T19:00:00-05:00) it's that exact moment; without one (2026-10-24T19:00) it's read in the organization's timezone.
durationMinutesinteger
locationTypeONLINE | LAN
venueIdstring | nullRequired when locationType is LAN.
notesstring | null
detailsobjectEvent details keyed by field id or label (any case), e.g. { "Maps": ["Ascent", "Bind"], "Lobby code": "NOVA-4471" }. Replaces every detail on the event. See GET /event-details.
Example response (200)
{
  "data": {
    "id": "cmsess_1",
    "object": "event",
    "type": "SCRIM",
    "eventType": null,
    "team": {
      "id": "cmteam_val",
      "name": "Nova Valorant",
      "game": "Valorant"
    },
    "audience": null,
    "opponent": {
      "id": "cmopp_apex",
      "name": "Apex Legionnaires"
    },
    "scheduledAt": "2026-10-23T00:00:00.000Z",
    "endsAt": "2026-10-23T01:30:00.000Z",
    "durationMinutes": 90,
    "timezone": "America/Chicago",
    "location": {
      "type": "ONLINE",
      "venue": null
    },
    "result": {
      "outcome": "WIN",
      "scoreFor": 13,
      "scoreAgainst": 9
    },
    "notes": null,
    "details": [
      {
        "fieldId": "cmfld_lobby",
        "label": "Lobby code",
        "type": "TEXT",
        "value": "NOVA-4471"
      }
    ],
    "url": "https://formation.example/nova-esports/schedule/practice/cmsess_1",
    "createdAt": "2026-10-20T18:05:00.000Z",
    "updatedAt": "2026-10-23T02:01:00.000Z"
  }
}
PUT/api/v2/events/{eventId}/result

Record a scrim result

Scope schedule:write

Body (JSON)

FieldTypeNotes
outcome *WIN | LOSS | DRAW | nullNull clears the result.
scoreForinteger | null
scoreAgainstinteger | null
Example response (200)
{
  "data": {
    "id": "cmsess_1",
    "object": "event",
    "type": "SCRIM",
    "eventType": null,
    "team": {
      "id": "cmteam_val",
      "name": "Nova Valorant",
      "game": "Valorant"
    },
    "audience": null,
    "opponent": {
      "id": "cmopp_apex",
      "name": "Apex Legionnaires"
    },
    "scheduledAt": "2026-10-23T00:00:00.000Z",
    "endsAt": "2026-10-23T01:30:00.000Z",
    "durationMinutes": 90,
    "timezone": "America/Chicago",
    "location": {
      "type": "ONLINE",
      "venue": null
    },
    "result": {
      "outcome": "WIN",
      "scoreFor": 13,
      "scoreAgainst": 9
    },
    "notes": null,
    "details": [
      {
        "fieldId": "cmfld_lobby",
        "label": "Lobby code",
        "type": "TEXT",
        "value": "NOVA-4471"
      }
    ],
    "url": "https://formation.example/nova-esports/schedule/practice/cmsess_1",
    "createdAt": "2026-10-20T18:05:00.000Z",
    "updatedAt": "2026-10-23T02:01:00.000Z"
  }
}
PATCH/api/v2/events/{eventId}/details

Change one or more event details

Only touches the details you name — set `null` to clear one, and leave the rest exactly as they are. See the same endpoint on matches.

Scope schedule:write

Body (JSON)

FieldTypeNotes
details *objectOnly the details to change, keyed by field id or label (any case). A value sets that detail; null clears it. Details you leave out stay as they are.
Example response (200)
{
  "data": {
    "id": "cmsess_1",
    "object": "event",
    "type": "SCRIM",
    "eventType": null,
    "team": {
      "id": "cmteam_val",
      "name": "Nova Valorant",
      "game": "Valorant"
    },
    "audience": null,
    "opponent": {
      "id": "cmopp_apex",
      "name": "Apex Legionnaires"
    },
    "scheduledAt": "2026-10-23T00:00:00.000Z",
    "endsAt": "2026-10-23T01:30:00.000Z",
    "durationMinutes": 90,
    "timezone": "America/Chicago",
    "location": {
      "type": "ONLINE",
      "venue": null
    },
    "result": {
      "outcome": "WIN",
      "scoreFor": 13,
      "scoreAgainst": 9
    },
    "notes": null,
    "details": [
      {
        "fieldId": "cmfld_lobby",
        "label": "Lobby code",
        "type": "TEXT",
        "value": "NOVA-4471"
      }
    ],
    "url": "https://formation.example/nova-esports/schedule/practice/cmsess_1",
    "createdAt": "2026-10-20T18:05:00.000Z",
    "updatedAt": "2026-10-23T02:01:00.000Z"
  }
}
GET/api/v2/matches/{matchId}/lineup

Get a match's lineup

Scope schedule:read

Example response (200)
{
  "data": {
    "object": "lineup",
    "matchId": "cmmatch_1",
    "starters": [
      {
        "membershipId": "cmmem_ava",
        "name": "ava#NA1",
        "position": "Duelist"
      },
      {
        "membershipId": "cmmem_jordan",
        "name": "jordan",
        "position": "IGL"
      }
    ],
    "subs": [
      {
        "membershipId": "cmmem_ethan",
        "name": "ethan",
        "position": null
      }
    ]
  }
}
PUT/api/v2/matches/{matchId}/lineup

Set a match's lineup

Replaces the whole lineup. Starters are kept in the order sent. Send empty lists to clear it.

Scope schedule:write

Body (JSON)

FieldTypeNotes
starters *object[]In order. Everyone must be on the team's roster.
subsobject[]Default: []
notifybooleanTell players who are newly in the lineup, or moved between starting and subbing.Default: true
Example response (200)
{
  "data": {
    "object": "lineup",
    "matchId": "cmmatch_1",
    "starters": [
      {
        "membershipId": "cmmem_ava",
        "name": "ava#NA1",
        "position": "Duelist"
      },
      {
        "membershipId": "cmmem_jordan",
        "name": "jordan",
        "position": "IGL"
      }
    ],
    "subs": [
      {
        "membershipId": "cmmem_ethan",
        "name": "ethan",
        "position": null
      }
    ]
  }
}
GET/api/v2/events/{eventId}/lineup

Get a scrim's lineup

Scope schedule:read

Example response (200)
{
  "data": {
    "object": "lineup",
    "matchId": "cmmatch_1",
    "starters": [
      {
        "membershipId": "cmmem_ava",
        "name": "ava#NA1",
        "position": "Duelist"
      },
      {
        "membershipId": "cmmem_jordan",
        "name": "jordan",
        "position": "IGL"
      }
    ],
    "subs": [
      {
        "membershipId": "cmmem_ethan",
        "name": "ethan",
        "position": null
      }
    ]
  }
}
PUT/api/v2/events/{eventId}/lineup

Set a scrim's lineup

Scope schedule:write

Body (JSON)

FieldTypeNotes
starters *object[]In order. Everyone must be on the team's roster.
subsobject[]Default: []
notifybooleanTell players who are newly in the lineup, or moved between starting and subbing.Default: true
Example response (200)
{
  "data": {
    "object": "lineup",
    "matchId": "cmmatch_1",
    "starters": [
      {
        "membershipId": "cmmem_ava",
        "name": "ava#NA1",
        "position": "Duelist"
      },
      {
        "membershipId": "cmmem_jordan",
        "name": "jordan",
        "position": "IGL"
      }
    ],
    "subs": [
      {
        "membershipId": "cmmem_ethan",
        "name": "ethan",
        "position": null
      }
    ]
  }
}
DELETE/api/v2/events/{eventId}

Delete an event

Scope schedule:write

Returns 204 with no body.

Attendance

Who's coming. Statuses: INVITED (no answer yet), CONFIRMED, DECLINED, then ATTENDED, ABSENT or LATE afterwards.

GET/api/v2/matches/{matchId}/attendance

A match's attendance

Scope attendance:read

Example response (200)
{
  "data": [
    {
      "object": "attendance",
      "membershipId": "cmmem_ava",
      "name": "Ava Nguyen",
      "status": "CONFIRMED",
      "respondedAt": "2026-10-21T14:12:00.000Z",
      "note": null
    }
  ]
}
PUT/api/v2/matches/{matchId}/attendance/{membershipId}

Set someone's match attendance

Adds them to the list if they're on the team's roster but not on it yet.

Scope attendance:write

Body (JSON)

FieldTypeNotes
status *INVITED | CONFIRMED | DECLINED | ATTENDED | ABSENT | LATE
notestring | null
Example response (200)
{
  "data": {
    "object": "attendance",
    "membershipId": "cmmem_ava",
    "name": "Ava Nguyen",
    "status": "CONFIRMED",
    "respondedAt": "2026-10-21T14:12:00.000Z",
    "note": null
  }
}
GET/api/v2/events/{eventId}/attendance

An event's attendance

Scope attendance:read

Example response (200)
{
  "data": [
    {
      "object": "attendance",
      "membershipId": "cmmem_ava",
      "name": "Ava Nguyen",
      "status": "CONFIRMED",
      "respondedAt": "2026-10-21T14:12:00.000Z",
      "note": null
    }
  ]
}
PUT/api/v2/events/{eventId}/attendance/{membershipId}

Set someone's event attendance

Scope attendance:write

Body (JSON)

FieldTypeNotes
status *INVITED | CONFIRMED | DECLINED | ATTENDED | ABSENT | LATE
notestring | null
Example response (200)
{
  "data": {
    "object": "attendance",
    "membershipId": "cmmem_ava",
    "name": "Ava Nguyen",
    "status": "CONFIRMED",
    "respondedAt": "2026-10-21T14:12:00.000Z",
    "note": null
  }
}

Opponents

Teams you play against, and what your scouting reports say about them.

GET/api/v2/opponents

List opponents

Scope opponents:read

Query

  • q (string) — Search by name.
  • limit (integer) — Items per page, 1–100. Default 25.
  • cursor (string) — The nextCursor from the previous page.
Example response (200)
{
  "data": [
    {
      "id": "cmopp_apex",
      "object": "opponent",
      "name": "Apex Legionnaires",
      "logoUrl": null,
      "contact": "apex#0001 on Discord",
      "notes": "Strong on Lotus.",
      "linkedTeamId": null,
      "createdAt": "2026-04-02T19:00:00.000Z",
      "updatedAt": "2026-04-02T19:00:00.000Z"
    }
  ],
  "pagination": {
    "limit": 25,
    "nextCursor": null,
    "hasMore": false
  }
}
POST/api/v2/opponents

Add an opponent

Scope opponents:write

Body (JSON)

FieldTypeNotes
name *string
logoUrlurl | null
contactstring | null
notesstring | null
Example response (201)
{
  "data": {
    "id": "cmopp_apex",
    "object": "opponent",
    "name": "Apex Legionnaires",
    "logoUrl": null,
    "contact": "apex#0001 on Discord",
    "notes": "Strong on Lotus.",
    "linkedTeamId": null,
    "createdAt": "2026-04-02T19:00:00.000Z",
    "updatedAt": "2026-04-02T19:00:00.000Z"
  }
}
GET/api/v2/opponents/{opponentId}

Get an opponent

Scope opponents:read

Example response (200)
{
  "data": {
    "id": "cmopp_apex",
    "object": "opponent",
    "name": "Apex Legionnaires",
    "logoUrl": null,
    "contact": "apex#0001 on Discord",
    "notes": "Strong on Lotus.",
    "linkedTeamId": null,
    "createdAt": "2026-04-02T19:00:00.000Z",
    "updatedAt": "2026-04-02T19:00:00.000Z"
  }
}
PATCH/api/v2/opponents/{opponentId}

Edit an opponent

Scope opponents:write

Body (JSON)

FieldTypeNotes
namestring
logoUrlurl | null
contactstring | null
notesstring | null
Example response (200)
{
  "data": {
    "id": "cmopp_apex",
    "object": "opponent",
    "name": "Apex Legionnaires",
    "logoUrl": null,
    "contact": "apex#0001 on Discord",
    "notes": "Strong on Lotus.",
    "linkedTeamId": null,
    "createdAt": "2026-04-02T19:00:00.000Z",
    "updatedAt": "2026-04-02T19:00:00.000Z"
  }
}
GET/api/v2/opponents/{opponentId}/reports

List scouting reports

What your teams have seen this opponent play: characters (agents, heroes… — characterLabel names them for the team's game), strats, players, notes and screenshots. Newest first.

Scope opponents:read

Query

  • teamId (comma-separated) — Only reports written for these teams.
Example response (200)
{
  "data": [
    {
      "id": "cmrep_1",
      "object": "scouting_report",
      "opponentId": "cmopp_apex",
      "team": {
        "id": "cmteam_val",
        "name": "Nova Valorant",
        "game": "Valorant"
      },
      "matchId": "cmmatch_1",
      "eventId": null,
      "title": null,
      "observedAt": "2026-10-24T00:00:00.000Z",
      "characterLabel": "Agents",
      "characters": [
        "Jett",
        "Omen",
        "Sova",
        "Killjoy",
        "Skye"
      ],
      "strats": [
        {
          "text": "Fast A split off a double smoke",
          "map": "Lotus"
        },
        {
          "text": "Stack B on defense after losing pistol",
          "map": null
        }
      ],
      "players": [
        "apex.kai",
        "apex.rin"
      ],
      "notes": "Their Jett takes every early duel — hold off-angles.",
      "screenshots": [
        {
          "id": "cmimg_1",
          "fileName": "scoreboard.png",
          "fileSize": 482113,
          "url": "https://formation.example/api/v2/scouting/images/cmimg_1"
        }
      ],
      "createdBy": {
        "membershipId": "cmmem_jordan",
        "name": "Jordan Lee"
      },
      "createdAt": "2026-10-24T02:10:00.000Z",
      "updatedAt": "2026-10-24T02:10:00.000Z"
    }
  ]
}
GET/api/v2/scouting/images/{imageId}

Download a scouting screenshot

The image file itself (PNG, JPEG, WebP or GIF), not JSON. Each report lists its screenshots' urls.

Scope opponents:read

Returns the file itself.

Announcements

Posting one notifies its audience and posts to Discord, like posting it in Formation.

GET/api/v2/announcements

List announcements

Newest first.

Scope announcements:read

Query

  • teamId (comma-separated) — One or more team ids (comma-separated).
  • orgWide (boolean) — true for organization-wide announcements only.
  • pinned (boolean) — Pinned or not.
  • published (boolean) — false for ones scheduled for later.
  • since (date-time) — Created at or after this time.
  • limit (integer) — Items per page, 1–100. Default 25.
  • cursor (string) — The nextCursor from the previous page.
Example response (200)
{
  "data": [
    {
      "id": "cmann_1",
      "object": "announcement",
      "title": "Tryouts this Saturday",
      "body": "Bring your own headset. Check-in at 10 AM.",
      "team": null,
      "pinned": true,
      "published": true,
      "publishAt": null,
      "author": {
        "membershipId": "cmmem_jordan",
        "name": "Jordan Lee"
      },
      "createdAt": "2026-10-18T16:00:00.000Z",
      "updatedAt": "2026-10-18T16:00:00.000Z"
    }
  ],
  "pagination": {
    "limit": 25,
    "nextCursor": null,
    "hasMore": false
  }
}
POST/api/v2/announcements

Post an announcement

Scope announcements:write

Body (JSON)

FieldTypeNotes
title *string
body *string
teamIdstring | nullPost to one team. Leave out for the whole organization.
pinnedbooleanDefault: false
publishAtstring | nullHold it back until this time. Leave out to publish now.
notifybooleanNotify the people involved and post to Discord, the same as scheduling in Formation does.Default: true
Example response (201)
{
  "data": {
    "id": "cmann_1",
    "object": "announcement",
    "title": "Tryouts this Saturday",
    "body": "Bring your own headset. Check-in at 10 AM.",
    "team": null,
    "pinned": true,
    "published": true,
    "publishAt": null,
    "author": {
      "membershipId": "cmmem_jordan",
      "name": "Jordan Lee"
    },
    "createdAt": "2026-10-18T16:00:00.000Z",
    "updatedAt": "2026-10-18T16:00:00.000Z"
  }
}
GET/api/v2/announcements/{announcementId}

Get an announcement

Scope announcements:read

Example response (200)
{
  "data": {
    "id": "cmann_1",
    "object": "announcement",
    "title": "Tryouts this Saturday",
    "body": "Bring your own headset. Check-in at 10 AM.",
    "team": null,
    "pinned": true,
    "published": true,
    "publishAt": null,
    "author": {
      "membershipId": "cmmem_jordan",
      "name": "Jordan Lee"
    },
    "createdAt": "2026-10-18T16:00:00.000Z",
    "updatedAt": "2026-10-18T16:00:00.000Z"
  }
}

Strategies

Each team's playbook.

GET/api/v2/strategies

List strategies

Most recently edited first.

Scope strategies:read

Query

  • teamId (comma-separated) — One or more team ids (comma-separated).
  • map (string) — Only strategies for this map or category.
  • limit (integer) — Items per page, 1–100. Default 25.
  • cursor (string) — The nextCursor from the previous page.
Example response (200)
{
  "data": [
    {
      "id": "cmstrat_1",
      "object": "strategy",
      "team": {
        "id": "cmteam_val",
        "name": "Nova Valorant",
        "game": "Valorant"
      },
      "title": "Lotus A split",
      "map": "Lotus",
      "notes": "Smoke tree and stairs, two through root...",
      "createdBy": {
        "membershipId": "cmmem_jordan",
        "name": "Jordan Lee"
      },
      "createdAt": "2026-09-01T19:00:00.000Z",
      "updatedAt": "2026-09-05T21:30:00.000Z"
    }
  ],
  "pagination": {
    "limit": 25,
    "nextCursor": null,
    "hasMore": false
  }
}
POST/api/v2/strategies

Add a strategy

Scope strategies:write

Body (JSON)

FieldTypeNotes
teamId *string
title *string
mapstring | null
notesstring | null
Example response (201)
{
  "data": {
    "id": "cmstrat_1",
    "object": "strategy",
    "team": {
      "id": "cmteam_val",
      "name": "Nova Valorant",
      "game": "Valorant"
    },
    "title": "Lotus A split",
    "map": "Lotus",
    "notes": "Smoke tree and stairs, two through root...",
    "createdBy": {
      "membershipId": "cmmem_jordan",
      "name": "Jordan Lee"
    },
    "createdAt": "2026-09-01T19:00:00.000Z",
    "updatedAt": "2026-09-05T21:30:00.000Z"
  }
}
GET/api/v2/strategies/{strategyId}

Get a strategy

Scope strategies:read

Example response (200)
{
  "data": {
    "id": "cmstrat_1",
    "object": "strategy",
    "team": {
      "id": "cmteam_val",
      "name": "Nova Valorant",
      "game": "Valorant"
    },
    "title": "Lotus A split",
    "map": "Lotus",
    "notes": "Smoke tree and stairs, two through root...",
    "createdBy": {
      "membershipId": "cmmem_jordan",
      "name": "Jordan Lee"
    },
    "createdAt": "2026-09-01T19:00:00.000Z",
    "updatedAt": "2026-09-05T21:30:00.000Z"
  }
}
PATCH/api/v2/strategies/{strategyId}

Edit a strategy

Scope strategies:write

Body (JSON)

FieldTypeNotes
titlestring
mapstring | null
notesstring | null
Example response (200)
{
  "data": {
    "id": "cmstrat_1",
    "object": "strategy",
    "team": {
      "id": "cmteam_val",
      "name": "Nova Valorant",
      "game": "Valorant"
    },
    "title": "Lotus A split",
    "map": "Lotus",
    "notes": "Smoke tree and stairs, two through root...",
    "createdBy": {
      "membershipId": "cmmem_jordan",
      "name": "Jordan Lee"
    },
    "createdAt": "2026-09-01T19:00:00.000Z",
    "updatedAt": "2026-09-05T21:30:00.000Z"
  }
}
DELETE/api/v2/strategies/{strategyId}

Delete a strategy

Scope strategies:write

Returns 204 with no body.

Recruitment

Prospects on your recruitment board. Stages: SCOUTING, CONTACTED, TRYOUT, OFFER, SIGNED, PASSED.

GET/api/v2/recruitment/levels

List recruitment levels

Scope recruitment:read

Example response (200)
{
  "data": [
    {
      "id": "cmlvl_college",
      "object": "recruitment_level",
      "name": "College",
      "order": 1
    }
  ]
}
GET/api/v2/recruitment/prospects

List prospects

Most recently changed first.

Scope recruitment:read

Query

  • stage (SCOUTING | CONTACTED | TRYOUT | OFFER | SIGNED | PASSED) — Board stage.
  • teamId (comma-separated) — One or more team ids (comma-separated).
  • levelId (comma-separated) — One or more recruitment levels.
  • game (string) — Only prospects for this game.
  • q (string) — Search by name or school/organization.
  • updatedSince (date-time) — Only items changed at or after this time — for syncing.
  • limit (integer) — Items per page, 1–100. Default 25.
  • cursor (string) — The nextCursor from the previous page.
Example response (200)
{
  "data": [
    {
      "id": "cmpros_1",
      "object": "prospect",
      "name": "Marco Diaz",
      "game": "Valorant",
      "stage": "TRYOUT",
      "level": {
        "id": "cmlvl_college",
        "name": "College"
      },
      "team": {
        "id": "cmteam_val",
        "name": "Nova Valorant"
      },
      "contact": {
        "email": "[email protected]",
        "phone": null,
        "discord": "marco"
      },
      "schoolOrOrg": "Midwest State University",
      "statsLinks": [
        {
          "label": "Tracker",
          "url": "https://tracker.gg/valorant/profile/riot/marco%23NA1"
        }
      ],
      "socialLinks": [],
      "notes": null,
      "assignedTo": {
        "membershipId": "cmmem_jordan",
        "name": "Jordan Lee"
      },
      "createdAt": "2026-08-20T19:00:00.000Z",
      "updatedAt": "2026-10-01T15:30:00.000Z"
    }
  ],
  "pagination": {
    "limit": 25,
    "nextCursor": null,
    "hasMore": false
  }
}
POST/api/v2/recruitment/prospects

Add a prospect

Scope recruitment:write

Body (JSON)

FieldTypeNotes
name *string
gamestring | nullDefaults to the team's game, if teamId is given.
levelIdstring | nullSee GET /recruitment/levels.
teamIdstring | null
emailemail | null
phonestring | null
discordHandlestring | null
schoolOrOrgstring | null
statsLinksobject[]
socialLinksobject[]
notesstring | null
assignedToMembershipIdstring | null
stageSCOUTING | CONTACTED | TRYOUT | OFFER | SIGNED | PASSEDDefault: "SCOUTING"
Example response (201)
{
  "data": {
    "id": "cmpros_1",
    "object": "prospect",
    "name": "Marco Diaz",
    "game": "Valorant",
    "stage": "TRYOUT",
    "level": {
      "id": "cmlvl_college",
      "name": "College"
    },
    "team": {
      "id": "cmteam_val",
      "name": "Nova Valorant"
    },
    "contact": {
      "email": "[email protected]",
      "phone": null,
      "discord": "marco"
    },
    "schoolOrOrg": "Midwest State University",
    "statsLinks": [
      {
        "label": "Tracker",
        "url": "https://tracker.gg/valorant/profile/riot/marco%23NA1"
      }
    ],
    "socialLinks": [],
    "notes": null,
    "assignedTo": {
      "membershipId": "cmmem_jordan",
      "name": "Jordan Lee"
    },
    "createdAt": "2026-08-20T19:00:00.000Z",
    "updatedAt": "2026-10-01T15:30:00.000Z"
  }
}
GET/api/v2/recruitment/prospects/{prospectId}

Get a prospect

Scope recruitment:read

Query

  • include (history) — history adds every stage change.
Example response (200)
{
  "data": {
    "id": "cmpros_1",
    "object": "prospect",
    "name": "Marco Diaz",
    "game": "Valorant",
    "stage": "TRYOUT",
    "level": {
      "id": "cmlvl_college",
      "name": "College"
    },
    "team": {
      "id": "cmteam_val",
      "name": "Nova Valorant"
    },
    "contact": {
      "email": "[email protected]",
      "phone": null,
      "discord": "marco"
    },
    "schoolOrOrg": "Midwest State University",
    "statsLinks": [
      {
        "label": "Tracker",
        "url": "https://tracker.gg/valorant/profile/riot/marco%23NA1"
      }
    ],
    "socialLinks": [],
    "notes": null,
    "assignedTo": {
      "membershipId": "cmmem_jordan",
      "name": "Jordan Lee"
    },
    "createdAt": "2026-08-20T19:00:00.000Z",
    "updatedAt": "2026-10-01T15:30:00.000Z"
  }
}
PATCH/api/v2/recruitment/prospects/{prospectId}

Edit or move a prospect

Changing stage is recorded in the prospect's history and tells whoever's assigned.

Scope recruitment:write

Body (JSON)

FieldTypeNotes
namestring
gamestring | nullDefaults to the team's game, if teamId is given.
levelIdstring | nullSee GET /recruitment/levels.
teamIdstring | null
emailemail | null
phonestring | null
discordHandlestring | null
schoolOrOrgstring | null
statsLinksobject[]
socialLinksobject[]
notesstring | null
assignedToMembershipIdstring | null
stageSCOUTING | CONTACTED | TRYOUT | OFFER | SIGNED | PASSEDMoving stages is recorded in the prospect's history.
stageNotestringA note for the history entry when the stage changes.
Example response (200)
{
  "data": {
    "id": "cmpros_1",
    "object": "prospect",
    "name": "Marco Diaz",
    "game": "Valorant",
    "stage": "TRYOUT",
    "level": {
      "id": "cmlvl_college",
      "name": "College"
    },
    "team": {
      "id": "cmteam_val",
      "name": "Nova Valorant"
    },
    "contact": {
      "email": "[email protected]",
      "phone": null,
      "discord": "marco"
    },
    "schoolOrOrg": "Midwest State University",
    "statsLinks": [
      {
        "label": "Tracker",
        "url": "https://tracker.gg/valorant/profile/riot/marco%23NA1"
      }
    ],
    "socialLinks": [],
    "notes": null,
    "assignedTo": {
      "membershipId": "cmmem_jordan",
      "name": "Jordan Lee"
    },
    "createdAt": "2026-08-20T19:00:00.000Z",
    "updatedAt": "2026-10-01T15:30:00.000Z"
  }
}

Webhooks

Have Formation call your tool the moment something changes, instead of polling. A webhook only sees events its key could already read. Each key manages its own webhooks — they're removed when the key is revoked. `match.created` (schedule:read) — A match was scheduled. `match.updated` (schedule:read) — A match was edited — its time, opponent, notes or event details (changes lists which). `match.result_recorded` (schedule:read) — A match was marked live, completed, cancelled or forfeited, or its score changed. `match.deleted` (schedule:read) — A match was deleted. `event.created` (schedule:read) — A practice, scrim or event was scheduled. `event.updated` (schedule:read) — A practice, scrim or event was edited (changes lists what). `event.result_recorded` (schedule:read) — A scrim's result was recorded or cleared. `event.deleted` (schedule:read) — A practice, scrim or event was deleted. `lineup.updated` (schedule:read) — A match or scrim lineup was saved. `attendance.updated` (attendance:read) — Someone RSVP'd, or was marked attended, absent or late. `announcement.published` (announcements:read) — An announcement went out (now, or at its scheduled time).

GET/api/v2/webhooks

List your webhooks

Scope webhooks:read

Example response (200)
{
  "data": [
    {
      "id": "cmwh_1",
      "object": "webhook",
      "name": "ValRAI",
      "url": "https://raiapp.party/formation/webhook",
      "events": [
        "match.updated",
        "event.updated"
      ],
      "enabled": true,
      "disabledReason": null,
      "failedAttemptsInARow": 0,
      "lastDeliveryAt": "2026-10-24T02:11:00.000Z",
      "createdAt": "2026-10-20T18:00:00.000Z"
    }
  ]
}
POST/api/v2/webhooks

Register a webhook

The signing secret is only ever shown in this response — store it to verify deliveries. Up to 20 per organization.

Scope webhooks:write

Body (JSON)

FieldTypeNotes
name *string
url *stringWhere to POST. Must be https.
events *string[]Event types to receive, e.g. ["match.updated", "event.updated"]. See the Webhooks section for the list.
Example response (201)
{
  "data": {
    "id": "cmwh_1",
    "object": "webhook",
    "name": "ValRAI",
    "url": "https://raiapp.party/formation/webhook",
    "events": [
      "match.updated",
      "event.updated"
    ],
    "enabled": true,
    "disabledReason": null,
    "failedAttemptsInARow": 0,
    "lastDeliveryAt": null,
    "createdAt": "2026-10-20T18:00:00.000Z",
    "secret": "whsec_8f2b1c7e4a9d60f3b1e7c2a9d4f60b8e"
  }
}
GET/api/v2/webhooks/{webhookId}

Get a webhook

Scope webhooks:read

Example response (200)
{
  "data": {
    "id": "cmwh_1",
    "object": "webhook",
    "name": "ValRAI",
    "url": "https://raiapp.party/formation/webhook",
    "events": [
      "match.updated",
      "event.updated"
    ],
    "enabled": true,
    "disabledReason": null,
    "failedAttemptsInARow": 0,
    "lastDeliveryAt": "2026-10-24T02:11:00.000Z",
    "createdAt": "2026-10-20T18:00:00.000Z"
  }
}
PATCH/api/v2/webhooks/{webhookId}

Edit a webhook

Set enabled: true to turn one back on after Formation disabled it for failing repeatedly.

Scope webhooks:write

Body (JSON)

FieldTypeNotes
namestring
urlstringWhere to POST. Must be https.
eventsstring[]Event types to receive, e.g. ["match.updated", "event.updated"]. See the Webhooks section for the list.
enabledbooleanTurn it back on after Formation switched it off, or pause it.
Example response (200)
{
  "data": {
    "id": "cmwh_1",
    "object": "webhook",
    "name": "ValRAI",
    "url": "https://raiapp.party/formation/webhook",
    "events": [
      "match.updated",
      "event.updated"
    ],
    "enabled": true,
    "disabledReason": null,
    "failedAttemptsInARow": 0,
    "lastDeliveryAt": "2026-10-24T02:11:00.000Z",
    "createdAt": "2026-10-20T18:00:00.000Z"
  }
}
DELETE/api/v2/webhooks/{webhookId}

Remove a webhook

Scope webhooks:write

Returns 204 with no body.

POST/api/v2/webhooks/{webhookId}/test

Send a test delivery

Sends a `ping` event right away so you can check your endpoint and signature verification.

Scope webhooks:write

Example response (200)
{
  "data": {
    "id": "cmwhd_1",
    "object": "webhook_delivery",
    "event": "match.updated",
    "status": "SUCCEEDED",
    "attempts": 1,
    "responseStatus": 200,
    "error": null,
    "nextAttemptAt": null,
    "createdAt": "2026-10-24T02:10:58.000Z",
    "deliveredAt": "2026-10-24T02:10:59.000Z",
    "payload": {
      "id": "evt_7e1c2a4e9f3b4d6a8c0e2f4b6d8a0c2e",
      "type": "match.updated",
      "createdAt": "2026-10-24T02:10:58.000Z",
      "organization": {
        "id": "cmorg_nova",
        "slug": "nova-esports"
      },
      "triggeredBy": {
        "type": "api_key",
        "apiKeyId": "cmkey_1",
        "apiKeyName": "ValRAI",
        "membershipId": "cmmem_jordan",
        "name": "Jordan Lee"
      },
      "data": {
        "object": {
          "id": "cmmatch_1",
          "object": "match",
          "team": {
            "id": "cmteam_val",
            "name": "Nova Valorant",
            "game": "Valorant"
          },
          "opponent": {
            "id": "cmopp_apex",
            "name": "Apex Legionnaires"
          },
          "scheduledAt": "2026-10-24T00:00:00.000Z",
          "status": "SCHEDULED"
        },
        "changes": [
          "details"
        ],
        "changedDetails": [
          {
            "fieldId": "cmfld_vod",
            "label": "VOD review"
          }
        ]
      }
    }
  }
}
GET/api/v2/webhooks/{webhookId}/deliveries

List recent deliveries

Newest first, kept for 30 days. Each one's payload is the exact body that was sent (and re-sent, on retries).

Scope webhooks:read

Query

  • status (PENDING | SUCCEEDED | FAILED) — Delivery status.
  • limit (integer) — Items per page, 1–100. Default 25.
  • cursor (string) — The nextCursor from the previous page.
Example response (200)
{
  "data": [
    {
      "id": "cmwhd_1",
      "object": "webhook_delivery",
      "event": "match.updated",
      "status": "SUCCEEDED",
      "attempts": 1,
      "responseStatus": 200,
      "error": null,
      "nextAttemptAt": null,
      "createdAt": "2026-10-24T02:10:58.000Z",
      "deliveredAt": "2026-10-24T02:10:59.000Z",
      "payload": {
        "id": "evt_7e1c2a4e9f3b4d6a8c0e2f4b6d8a0c2e",
        "type": "match.updated",
        "createdAt": "2026-10-24T02:10:58.000Z",
        "organization": {
          "id": "cmorg_nova",
          "slug": "nova-esports"
        },
        "triggeredBy": {
          "type": "api_key",
          "apiKeyId": "cmkey_1",
          "apiKeyName": "ValRAI",
          "membershipId": "cmmem_jordan",
          "name": "Jordan Lee"
        },
        "data": {
          "object": {
            "id": "cmmatch_1",
            "object": "match",
            "team": {
              "id": "cmteam_val",
              "name": "Nova Valorant",
              "game": "Valorant"
            },
            "opponent": {
              "id": "cmopp_apex",
              "name": "Apex Legionnaires"
            },
            "scheduledAt": "2026-10-24T00:00:00.000Z",
            "status": "SCHEDULED"
          },
          "changes": [
            "details"
          ],
          "changedDetails": [
            {
              "fieldId": "cmfld_vod",
              "label": "VOD review"
            }
          ]
        }
      }
    }
  ],
  "pagination": {
    "limit": 25,
    "nextCursor": null,
    "hasMore": false
  }
}