How it works
This product is API-first today. The hosted and embedded options are designed but not built, so if you want staffing right now you'll be calling us server-to-server.
-
You create a job
Title, compensation, location, schedule window, and the credentials the worker must hold.
-
We match it against cleared people
The pool is everyone holding at least one verified, unexpired Clear2Join credential. That means "cleared for something" — not necessarily a background check. Pass
credentialDefinitionIdsto filter on the exact credential you require. -
You send offers and negotiate
Offer, counter, withdraw — all from your own system. You can set an
expiresAton an offer and we store it, but note that nothing expires it for you today; see Events to you. -
The worker does the job in our app
That half needs no integration from you, in any option.
The worker experience is always ours
However a job is created — hosted, embedded or API — the coach or staffer works it in The Good Game app. That's deliberate: it's how their clearance, ratings and payout history stay in one place across every organization that hires them.
Your options
| Way | Status | What you get | What you build |
|---|---|---|---|
| Hosted | Planned | We own job creation, dispatch and hiring for your events. | Nothing — you consume the staffed result. |
| Embedded / SDK | Planned | A hosted job-posting and candidate flow inside your UI. | A button and a completion handler. |
| API | Partly working |
Jobs and the offer webhooks work. Cleared-coach search and offer-send currently
return 500 — see the banner at the top of this page.
|
Your entire staffing UI. |
Hosted
Not built yet. Documented so you can tell us whether this is the shape you want — partner demand is what schedules it.
We staff your events Planned
The intended shape: you tell us an event needs staffing, and we own creating the jobs, dispatching them, negotiating, and confirming the hires. You consume the staffed roster.
This is the natural companion to hosted registration — if we're already running your events and know your schedule, we can staff them without you filing a single job. Until it exists, the API gives you the same outcome with you driving.
Embedded / SDK
Also not built yet.
Job-posting embed Planned
The intended shape: a hosted job-creation and candidate-review flow that opens inside your UI, so your staff can post and hire without you building the screens.
Jobs created this way would land in exactly the same dispatch pipeline as jobs created via the API or internally — there is one pipeline, and the option only changes who renders the form.
API
No UI of ours anywhere on your side. Your systems call ours with a key and you keep every pixel. This is the only option that exists today, and it is deployed and reachable in production.
Keys & scopes Partly working
We issue you a key; it's displayed once at creation and stored only as a hash. Send it
on every request as a bearer token or in X-API-Key. The same key also
covers clearance.
curl "{API_BASE_URL}/api/v1/partner/jobs" \
-H "Authorization: Bearer {API_KEY}"
# or
curl "{API_BASE_URL}/api/v1/partner/jobs" \
-H "X-API-Key: {API_KEY}"
| Scope | Grants |
|---|---|
coaches:read |
Search and read the cleared-coach pool. |
jobs:read / jobs:write |
Read / create, update, cancel and complete jobs. |
offers:read / offers:write |
Read / send, counter and withdraw offers. |
candidates:read / candidates:write |
Read / invite candidates into clearance — see Safety & Clearance. |
* |
Everything. Convenient for a first integration; ask for the narrow set before production. |
Scopes are recorded on your key, not yet enforced
We record the scopes you're issued, but staffing and clearance routes authenticate the key without checking them — a valid key reaches every partner endpoint regardless. So scopes are not a containment boundary today: store and rotate the key as if it could do everything, because it can. Still ask for the narrow set — enforcement is coming, and a key issued for what you actually use is the one that stays correct.
Key lifecycle
Keys can be scoped, given an expiry, and revoked independently. Multiple live keys per partner are fine — rotate by creating the new one, cutting over, then revoking the old.
Rate limit Not enforced
Your partner record carries a 60 req/min allowance, and that's the budget we'll enforce — but no limiter reads it today, so bursts currently succeed. Don't build a design that depends on more than 60; it will start being held to it.
Organization status
Your partner organization must be active. Suspended partners get
401 on every route, regardless of key validity.
Jobs, coaches & offers Partly working
| Method | Path | Purpose |
|---|---|---|
GET |
/api/v1/partner/coaches |
Search the cleared pool by credentialDefinitionIds,
locationState and city, with offset /
limit. A sport param is accepted but
currently ignored — it does not narrow the result set, and the
sports array comes back empty. Filter on credentials, not sport.
|
GET |
/api/v1/partner/coaches/:id |
One coach's public profile and clearance state. |
GET |
/api/v1/partner/jobs |
List your jobs. |
POST |
/api/v1/partner/jobs |
Create a job. |
GET |
/api/v1/partner/jobs/:id |
Read one job. |
PATCH |
/api/v1/partner/jobs/:id |
Update title, description, compensation or schedule. |
POST |
/api/v1/partner/jobs/:id/cancel |
Cancel a job. |
POST |
/api/v1/partner/jobs/:id/complete |
Mark a job complete. |
POST |
/api/v1/partner/jobs/:jobId/offers |
Send an offer to one or more coaches. |
GET |
/api/v1/partner/jobs/:jobId/offers |
List a job's offers. |
GET |
/api/v1/partner/offers/:id |
Read one offer. |
POST |
/api/v1/partner/offers/:id/counter |
Counter a coach's counter-offer. |
POST |
/api/v1/partner/offers/:id/withdraw |
Withdraw an offer. |
Creating a job
# compensationAmount is in cents when type is 'fixed'.
curl -X POST "{API_BASE_URL}/api/v1/partner/jobs" \
-H "Authorization: Bearer {API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"title": "Lead Coach — Tennis Camp, Session I",
"description": "Five mornings, ages 8-12.",
"compensationAmount": 45000,
"compensationAmountType": "fixed",
"locationCity": "Fort Worth",
"locationState": "TX",
"scheduledStart": "2026-08-03T14:00:00.000Z",
"scheduledEnd": "2026-08-07T21:00:00.000Z",
"credentialRequirements": [
{ "credentialDefinitionId": "71955371-4ed7-4ed6-a6a2-b3ef3536c719", "required": true }
],
"metadata": { "yourJobRef": "REQ-10421" }
}'
metadata on a job and externalRef on a candidate are stored
and returned untouched, which makes reconciliation on your side trivial.
Events to you Preview
Rather than polling for offer outcomes, give us an HTTPS endpoint and we push signed events to it. Full signing, retry and verification details are in Reference → Webhooks.
| Type | Fires when | Fires today? |
|---|---|---|
offer.accepted |
A coach accepts your offer in the TGG app. | Yes |
offer.declined |
A coach declines. | Yes |
offer.countered |
A coach counters on price — or you counter theirs. | Yes |
offer.withdrawn |
You withdraw an offer. | Yes |
offer.expired |
An offer's expiresAt passes unanswered. |
No |
payment.captured |
Funds captured for a job. | No |
payment.payout_sent |
Payout sent to the worker. | No |
payment.payout_failed |
A payout attempt failed. | No |
What the four "No" rows mean for you
The transport is fully live in production — signing, retries, the dead-letter queue and the two-minute drain all run today, and the four offer events above are emitted by real code paths, including the coach's own accept/decline/counter in the TGG app. So the negotiation loop closes without you polling anything.
offer.expired doesn't fire because nothing expires offers.
We store the expiresAt you send, but no job sweeps past it — an
unanswered offer stays pending indefinitely. If your UI shows an expiry,
enforce it on your side and call withdraw when your own clock runs out.
The three payment.* events have no emitter at all.
Payout state is real — a payout line is created when a coach accepts — but nothing
publishes it to you. Until it does, treat payout status as something you ask us about,
not something you receive.
Complete integrations
One path today, since the hosted and embedded options aren't built yet. It works end to end — just read the gaps in the steps rather than skimming them.
Staff your events from your own admin
In developmentNothing to do with registration. If you already run your own platform and just need cleared people, this is a pure server-to-server integration.
-
Get a key and the scopes you need
jobs:write,coaches:readandoffers:write. Ask for those three rather than*— see the note on enforcement. -
Create the job with the credentials it requires
See the job payload above.
-
Search the cleared pool and send offers
cURL
# Filter on the credential you actually require — `sport` is not applied today. curl "{API_BASE_URL}/api/v1/partner/coaches?locationState=TX&credentialDefinitionIds=71955371-4ed7-4ed6-a6a2-b3ef3536c719&limit=25" \ -H "Authorization: Bearer {API_KEY}" curl -X POST "{API_BASE_URL}/api/v1/partner/jobs/{job_id}/offers" \ -H "Authorization: Bearer {API_KEY}" \ -H "Content-Type: application/json" \ -d '{ "coachProfileIds": ["11112222-3333-4444-5555-666677778888"], "amount": 45000, "amountType": "fixed", "expiresAt": "2026-07-25T23:59:59.000Z" }' -
Take webhooks instead of polling
offer.accepted,offer.declined,offer.counteredandoffer.withdrawnall fire today, so your table stays current without a cron. Thepayment.*events do not fire yet — if you need payout state, ask us rather than waiting on a delivery that won't arrive. -
Complete the job when it's done
POST /partner/jobs/:id/completemoves the job tofilled. It does not trigger the payout — the worker's payout line is created earlier, the moment they accept your offer, and runs on our payout schedule from there. So completing a job is your bookkeeping signal, not a money instruction.
Need people in the pool who aren't there yet? Push them through candidate clearance first — same key, same organization.