Trainer Command Center API

Give people access to your team plans from your own CRM, checkout or automation tool, and get notified when your team changes. Built for CRMs, checkouts and no-code automation tools such as GoHighLevel and Zapier.

Base URLhttps://us-central1-trainer-command-center-prod.cloudfunctions.net/zapier_tcc_app_connector

With the API you can:

  • Add a person to one or more of your team plans, creating their account if they don't have one yet.
  • Remove a person from one plan, or from all of your plans at once.
  • Look up your team plans and the people on them.
  • Receive a webhook when someone joins or leaves a plan, or when a plan is created, changed or deleted.

Each trainer has their own API key, and a key only ever sees that trainer's own team plans and the people on them.

Quickstart

  1. Get your API key Sign in to Trainer Command Center, open your Profile, and under Zapier API key click Generate Api Key, then Copy Api Key. The same key works for direct API calls and for our Zapier app (the Zapier app is invite-only; see Zapier).
  2. Check the key works
    export TCC_API_KEY="paste-your-key-here"
    export TCC_API="https://us-central1-trainer-command-center-prod.cloudfunctions.net/zapier_tcc_app_connector"
    
    curl "$TCC_API/whoami" -H "Authorization: Bearer $TCC_API_KEY"
    A 200 with your email means you're connected. A 403 means the key is wrong; copy it again from your Profile.
  3. Find the team plan you want to grant
    curl "$TCC_API/team-plan" -H "Authorization: Bearer $TCC_API_KEY"
    This lists every plan you own. Note the id of the one you want.
  4. Add someone to it
    curl -X POST "$TCC_API/upsert-user" \
      -H "Authorization: Bearer $TCC_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"email":"jordan@example.com","firstName":"Jordan","lastName":"Reyes","teamPlanIds":["YOUR_TEAM_PLAN_ID"]}'
    If Jordan already has an account, the plan is added to it. If not, an account is created with the plan on it and Jordan is emailed an invitation to set up their login. Either way you get Jordan's record back. This is the live system, so test with your own email address or a teammate's, then remove the plan again with /remove-team-plan-from-user.

Authentication

Send your API key in the Authorization header on every request. Both of these forms work:

Authorization: Bearer YOUR_API_KEY
Authorization: YOUR_API_KEY

A request with no key, an unknown key or a disabled key gets 403 with the body Unauthorized. The response is the same in all three cases.

Keep the key secret. It can add and remove people from your team plans. Put it in your tool's secret or header settings, never in a URL, a page's source code or a shared document. If you think it has leaked, contact support to have it replaced.

What a key can see

A standard key has access level OWN_TEAM_PLANS. It can only grant, remove and list plans you own, and it can only look up people who are on one of your plans. Through the endpoints, plans belonging to other trainers are treated as if they don't exist.

Sending requests

Read requests use query parameters. Write requests are POST with a JSON object as the body. The body can be sent as application/json or as JSON text with text/plain, so most no-code tools work without extra configuration.

Fields in write requests

Every write endpoint reads the same four fields. Each one can be spelled in camelCase or snake_case:

FieldAlso accepted asNotes
emailcontact_email, user_emailRequired. The API converts it to lowercase before matching and storing, so send the address exactly as the person uses it.
firstNamefirst_nameOptional. Used only when a new account is created.
lastNamelast_nameOptional. Used only when a new account is created.
teamPlanIdsteam_plan_ids, teamPlanId, team_plan_idRequired on every write except remove-all. One or more team plan ids.

Team plan ids

You can send teamPlanIds in whichever form your tool can produce. All of these mean the same thing:

{ "teamPlanIds": ["PLAN_ID_A", "PLAN_ID_B"] }     // JSON array
{ "teamPlanIds": "PLAN_ID_A,PLAN_ID_B" }        // comma-separated text
{ "team_plan_id": "PLAN_ID_A" }                 // a single id

Ids can also be separated by line breaks. Duplicates are ignored.

Plans you can't use are skipped. Any id that isn't one of your active team plans (a typo, another trainer's plan, or a plan that is deactivated or deleted) is dropped from the request without an error. The request only fails, with Invalid team plan IDs, when none of the ids are usable.

Check the teamPlanIds in the response to confirm which plans the person actually holds.

Deactivated plans can't be changed through the API. While a plan is deactivated (for example, when its billing is overdue) you can't grant it or remove people from it, and its members don't have access through it. If the plan becomes active again, the people who were on it get their access back. To remove someone from a deactivated plan, do it in Trainer Command Center.

Fields nested under customData

If your tool wraps your fields in a customData object (GoHighLevel workflow webhooks do this), the API looks there too. A field at the top level takes priority over the same field inside customData.

Responses and errors

Successful responses are JSON. Error responses are plain text that says what went wrong, for example Failed to add team plan to user. User with email jordan@example.com does not exist.

StatusMeaning
200The request worked.
400The request couldn't be carried out. The body says why: missing email, no usable team plan ids, the person doesn't exist yet, the body isn't valid JSON, and so on.
403The API key is missing, unknown or disabled. Body: Unauthorized.
404A lookup found nothing you have access to. Body: Team plan not found or User not found.
5xx or no responseA temporary problem on our side. Wait and retry; set a client timeout of about 30 seconds so a stuck request doesn't hold up your workflow.

Grant and remove requests are safe to retry. Granting one of your active plans to someone who already holds it, or removing one they don't hold, changes nothing and still returns 200. POST /user is the exception: a retry after a successful create returns 400 because the account now exists.

Endpoints

GET/whoami

Confirms your key works and tells you which account it belongs to. Use it as the connection test in any integration.

Response 200
{
  "owner_id": "Kq81ZtR3VwLm5xPa0dNc",
  "owner_email": "coach@example.com",
  "access_level": "OWN_TEAM_PLANS"
}

GET/team-plan

Lists your team plans. With no parameters it returns all of them. Deleted plans are never included. Deactivated plans are included, but the response doesn't say which ones they are, and they can't be granted until they are active again; check a plan's status in Trainer Command Center if a grant returns Invalid team plan IDs.

Query parameterDescription
nameOptional. Exact plan name, including capitalisation and spaces.
idOptional. A team plan id.
curl "$TCC_API/team-plan?name=Inner%20Circle" -H "Authorization: Bearer $TCC_API_KEY"
Response 200
[
  {
    "id": "Pz4cW7nLq2Rk9TbXv1Hs",
    "name": "Inner Circle",
    "createdAt": 1727712000000,
    "createdById": "Kq81ZtR3VwLm5xPa0dNc",
    "ownerId": "Kq81ZtR3VwLm5xPa0dNc",
    "permissionLevelId": "stack_level"
  }
]
FieldDescription
idThe team plan id. Use this in write requests.
nameThe plan's name.
createdAtWhen the plan was created, in milliseconds since 1 January 1970 UTC.
createdByIdThe account that created the plan.
ownerIdThe account that owns the plan.
permissionLevelIdThe plan's level: core_level, stack_level or arsenal_level.

Returns 404 Team plan not found when nothing matches.

GET/user

Looks up a person by email and shows which of your plans they hold.

Query parameterDescription
emailRequired. Converted to lowercase before matching.
curl "$TCC_API/user?email=jordan@example.com" -H "Authorization: Bearer $TCC_API_KEY"
Response 200
[
  {
    "id": "Uv6mB0yQe3Js8FhK2wTa",
    "firstName": "Jordan",
    "lastName": "Reyes",
    "email": "jordan@example.com",
    "activeTeamPlanIds": ["Pz4cW7nLq2Rk9TbXv1Hs"],
    "activeCore4TeamPlanIds": [],
    "activeStackTeamPlanIds": ["Pz4cW7nLq2Rk9TbXv1Hs"],
    "activeArsenalTeamPlanIds": []
  }
]

The response is a list. activeTeamPlanIds holds the person's active plans that are yours; the three level lists split those same plans by level. Plans the person holds with other trainers are not shown.

Returns 404 User not found when no account has that email, or when the person isn't assigned to any of your plans (they were never added, or you have removed them from all of your plans). You get the same answer in both cases. Returns 400 when email is missing.

POST/upsert-user

The one to use for most integrations. Grants one or more of your plans to a person by email. If they don't have an account yet, it creates one. Use this when you don't know, or don't want to check, whether the person already exists.

New accounts get an invitation email. When this call creates an account, the person is emailed an invitation to set up their login, just as when you add them in Trainer Command Center. Use real addresses only, and let people know to expect it before you import a list.

Request body
{
  "email": "jordan@example.com",
  "firstName": "Jordan",
  "lastName": "Reyes",
  "teamPlanIds": ["Pz4cW7nLq2Rk9TbXv1Hs"]
}
Response 200
{
  "id": "Uv6mB0yQe3Js8FhK2wTa",
  "email": "jordan@example.com",
  "firstName": "Jordan",
  "lastName": "Reyes",
  "teamPlanIds": ["Pz4cW7nLq2Rk9TbXv1Hs"]
}

teamPlanIds in the response lists all of the person's active plans that are yours, not just the ones in this request. For an existing person, the names in the response are the ones already on their account; firstName and lastName in the request don't change them.

POST/user

Creates a new account with one or more of your plans on it, and emails the person an invitation to set up their login. Takes the same body as /upsert-user and returns the same shape.

Unlike /upsert-user, this fails if the email already has an account: 400 Failed to add user. User with email jordan@example.com already exists. Use it when creating a duplicate would be a mistake you want to hear about.

POST/add-team-plan-to-user

Grants one or more of your plans to a person who already has an account. Fails with 400 … does not exist if the email has no account; use /upsert-user if you want the account created instead.

Request body
{
  "email": "jordan@example.com",
  "teamPlanIds": ["Pz4cW7nLq2Rk9TbXv1Hs"]
}

Returns the person's record in the same shape as /upsert-user, with teamPlanIds showing all of their active plans that are yours after the change.

POST/remove-team-plan-from-user

Removes one or more of your plans from a person. Their account stays; only their access through those plans ends. Same body as /add-team-plan-to-user.

curl -X POST "$TCC_API/remove-team-plan-from-user" \
  -H "Authorization: Bearer $TCC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email":"jordan@example.com","teamPlanIds":["Pz4cW7nLq2Rk9TbXv1Hs"]}'

Returns the person's record with teamPlanIds showing the plans of yours they still hold.

POST/remove-all-team-plans-from-user

Removes every one of your active plans from a person in one call. Plans they hold with other trainers are not touched. Only email is needed.

Request body
{ "email": "jordan@example.com" }

Returns the person's record with teamPlanIds: []. Fails with 400 … does not exist if the email has no account.

Webhooks

Webhooks tell your system when something changes on your team, whether the change came through the API or someone made it in Trainer Command Center. Register a URL for each event you care about.

POST/zapier-hook

Registers a webhook. The path has "zapier" in it for historical reasons; any HTTPS URL that accepts a POST works.

Request body
{
  "hookUrl": "https://your-system.example.com/tcc-events/9f3c1a",
  "triggerKey": "team_plan_added"
}
Response 200
{ "hookId": "Hk2nW5qZr8Lt1VcPy7Ms" }

Keep the hookId; you need it to unsubscribe. One registration covers one event; register the same URL several times to receive several events. Returns 400 with the body Bad Request if either field is missing.

triggerKey must be spelled exactly as in the events table. A misspelled key is accepted and returns a hookId, but that hook never receives anything. There is no endpoint that lists your hooks, so store each hookId when you create it.

DELETE/zapier-hook/{hookId}

Removes a webhook registration. Returns 200, including when the hook had already been removed.

curl -X DELETE "$TCC_API/zapier-hook/Hk2nW5qZr8Lt1VcPy7Ms" -H "Authorization: Bearer $TCC_API_KEY"

Events

Use these values as triggerKey. You only receive events for team plans you own.

triggerKeySent whenPayload
team_plan_addedA person gains access to one of your plans, including when a deactivated plan becomes active again (one event per member).Member change
team_plan_removedA person loses access to one of your plans, including when the plan is deactivated or deleted.Member change
user_addedA new account is created holding one of your plans.Person
user_removedAn account holding one of your plans is deleted.Person
access_to_warrior_courses_has_been_grantedA person on one of your plans is given access to Warrior courses.Courses
access_to_warrior_courses_has_been_revokedA person on one of your plans loses access to Warrior courses, or their account is deleted.Courses
team_plan_added_by_userYou create a new team plan.Team plan
team_plan_updated_by_userOne of your plans changes: name, level, status, owner, permissions, cost or course access. Also sent once when a plan is created.Plan change
team_plan_deleted_by_userOne of your plans is deleted. A team_plan_updated_by_user event is sent as well, and this event can repeat if the deleted plan is edited again.Team plan

Member change payload

{
  "id": "Uv6mB0yQe3Js8FhK2wTa",
  "email": "jordan@example.com",
  "firstName": "Jordan",
  "lastName": "Reyes",
  "teamPlanId": "Pz4cW7nLq2Rk9TbXv1Hs",
  "teamPlanLevel": "Stack"
}

teamPlanLevel is the display name of the plan's level. A change that adds or removes several plans at once sends one event per plan.

Person payload

{
  "id": "Uv6mB0yQe3Js8FhK2wTa",
  "email": "jordan@example.com",
  "firstName": "Jordan",
  "lastName": "Reyes",
  "activeTeamPlanIds": ["Pz4cW7nLq2Rk9TbXv1Hs"]
}

activeTeamPlanIds lists the person's active team plans at the time of the event.

Courses payload

{
  "id": "Uv6mB0yQe3Js8FhK2wTa",
  "email": "jordan@example.com",
  "firstName": "Jordan",
  "lastName": "Reyes",
  "accessToWarriorCoursesHasBeenGranted": true
}

Team plan payload

{
  "id": "Pz4cW7nLq2Rk9TbXv1Hs",
  "name": "Inner Circle",
  "teamPlanLevel": "Stack",
  "status": "active",
  "permissions": [],
  "noCost": false,
  "createdAt": 1727712000000,
  "ownerId": "Kq81ZtR3VwLm5xPa0dNc",
  "ownerEmail": "coach@example.com",
  "ownerFirstName": "Sam",
  "ownerLastName": "Carter",
  "includeWarriorCourses": true
}

status is active, deactivated or deleted. A deactivated plan stops granting access until it is active again; this happens, for example, when a plan's billing is overdue.

Plan change payload

The plan as it was and as it is now, each in the team plan shape. before is left out when the plan has just been created.

{
  "id": "Pz4cW7nLq2Rk9TbXv1Hs",
  "before": { "name": "Inner Circle", "status": "active", … },
  "after":  { "name": "Inner Circle 2026", "status": "active", … }
}

Delivery

  • Each event is sent as an HTTP POST whose body is JSON text. The Content-Type header is text/plain, so parse the raw body as JSON rather than relying on the header.
  • Each event is sent once, and the status code your endpoint returns isn't checked. Failed deliveries are not retried, so if missing an event matters, check the person with GET /user when your system next acts on them.
  • Requests are not signed. Use a long, hard-to-guess URL for your endpoint, and treat the payload as a prompt to look up current state rather than as the final word.
  • Events are sent shortly after the change, and they aren't guaranteed to arrive in order.

GoHighLevel

To give someone access when they buy, book or get tagged in GoHighLevel:

  1. In your workflow, add a webhook action that lets you set request headers (in GoHighLevel this is the Custom Webhook action).
  2. Set the method to POST and the URL to https://us-central1-trainer-command-center-prod.cloudfunctions.net/zapier_tcc_app_connector/upsert-user.
  3. Add the header Authorization with the value Bearer YOUR_API_KEY.
  4. In the body, send email, first_name and last_name from the contact, and team_plan_ids with your plan id. To grant several plans, separate the ids with commas. Your plan ids come from GET /team-plan; if you can't run that call yourself, ask support for the ids of your plans.

GoHighLevel's own field names (first_name, contact_email), its customData wrapper and its habit of sending lists as text are all understood, so you can map contact fields directly. To take access away, point a second workflow at /remove-team-plan-from-user with the same body. This works while the plan is active; see deactivated plans.

Zapier

The Zapier app is private and invite-only. It isn't listed in Zapier's public app directory, so you can't find it by searching in Zapier. To use it, email our support team and ask for an invitation to the Trainer Command Center Zapier app, sent to the email address you sign in to Zapier with. Once you accept the invitation, the app appears in your Zapier account and you connect it with your API key.

Our Zapier app uses this same API and the same key. When you connect it with your key, its actions (add user, which calls /upsert-user; add or remove a team plan; remove all team plans), its searches (find a user, find a team plan) and its triggers map one-to-one onto the endpoints and events on this page. You can mix the two: for example, receive events in Zapier and make calls from your own system.

Your API key

  • Each trainer account has one key. Generate it from your Profile in Trainer Command Center; once generated, the same key is shown there for you to copy again.
  • Your key belongs to your account. Anyone holding it can act on your team plans, so share it only with tools and people you trust with that.
  • To replace a key that may have leaked, or to request broader access for a support or agency setup, contact the Trainer Command Center support team. Replacing a key disconnects every tool that uses the old one, including webhook registrations, so plan to reconnect them.