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.
https://us-central1-trainer-command-center-prod.cloudfunctions.net/zapier_tcc_app_connectorWith 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
- 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).
- Check the key works
A
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"
200with your email means you're connected. A403means the key is wrong; copy it again from your Profile. - Find the team plan you want to grant
This lists every plan you own. Note the
curl "$TCC_API/team-plan" -H "Authorization: Bearer $TCC_API_KEY"
idof the one you want. - Add someone to it
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
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"]}'/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:
| Field | Also accepted as | Notes |
|---|---|---|
email | contact_email, user_email | Required. The API converts it to lowercase before matching and storing, so send the address exactly as the person uses it. |
firstName | first_name | Optional. Used only when a new account is created. |
lastName | last_name | Optional. Used only when a new account is created. |
teamPlanIds | team_plan_ids, teamPlanId, team_plan_id | Required 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 idIds 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.
| Status | Meaning |
|---|---|
200 | The request worked. |
400 | The 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. |
403 | The API key is missing, unknown or disabled. Body: Unauthorized. |
404 | A lookup found nothing you have access to. Body: Team plan not found or User not found. |
5xx or no response | A 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.
{
"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 parameter | Description |
|---|---|
name | Optional. Exact plan name, including capitalisation and spaces. |
id | Optional. A team plan id. |
curl "$TCC_API/team-plan?name=Inner%20Circle" -H "Authorization: Bearer $TCC_API_KEY"
[
{
"id": "Pz4cW7nLq2Rk9TbXv1Hs",
"name": "Inner Circle",
"createdAt": 1727712000000,
"createdById": "Kq81ZtR3VwLm5xPa0dNc",
"ownerId": "Kq81ZtR3VwLm5xPa0dNc",
"permissionLevelId": "stack_level"
}
]| Field | Description |
|---|---|
id | The team plan id. Use this in write requests. |
name | The plan's name. |
createdAt | When the plan was created, in milliseconds since 1 January 1970 UTC. |
createdById | The account that created the plan. |
ownerId | The account that owns the plan. |
permissionLevelId | The 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 parameter | Description |
|---|---|
email | Required. Converted to lowercase before matching. |
curl "$TCC_API/user?email=jordan@example.com" -H "Authorization: Bearer $TCC_API_KEY"
[
{
"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.
{
"email": "jordan@example.com",
"firstName": "Jordan",
"lastName": "Reyes",
"teamPlanIds": ["Pz4cW7nLq2Rk9TbXv1Hs"]
}{
"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.
{
"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.
{ "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.
{
"hookUrl": "https://your-system.example.com/tcc-events/9f3c1a",
"triggerKey": "team_plan_added"
}{ "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.
| triggerKey | Sent when | Payload |
|---|---|---|
team_plan_added | A person gains access to one of your plans, including when a deactivated plan becomes active again (one event per member). | Member change |
team_plan_removed | A person loses access to one of your plans, including when the plan is deactivated or deleted. | Member change |
user_added | A new account is created holding one of your plans. | Person |
user_removed | An account holding one of your plans is deleted. | Person |
access_to_warrior_courses_has_been_granted | A person on one of your plans is given access to Warrior courses. | Courses |
access_to_warrior_courses_has_been_revoked | A person on one of your plans loses access to Warrior courses, or their account is deleted. | Courses |
team_plan_added_by_user | You create a new team plan. | Team plan |
team_plan_updated_by_user | One 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_user | One 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
POSTwhose body is JSON text. TheContent-Typeheader istext/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 /userwhen 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:
- In your workflow, add a webhook action that lets you set request headers (in GoHighLevel this is the Custom Webhook action).
- Set the method to
POSTand the URL tohttps://us-central1-trainer-command-center-prod.cloudfunctions.net/zapier_tcc_app_connector/upsert-user. - Add the header
Authorizationwith the valueBearer YOUR_API_KEY. - In the body, send
email,first_nameandlast_namefrom the contact, andteam_plan_idswith your plan id. To grant several plans, separate the ids with commas. Your plan ids come fromGET /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.