Staffing & Job Dispatch

Staffing & Job Dispatch In development

Tap our pool of Clear2Join-cleared coaches and staff to fill events fast. A job is a shift you need covered: a title, compensation, a location, a window, and the credentials the worker must hold. We match it against people who are already cleared, and you negotiate to a hire.

In development — read this before you scope anything

Don't build against this yet. The routes are deployed, but cleared-coach search and offer-send currently fail: they query a credentials.profileId column that no longer exists in the schema, so GET /partner/coaches, GET /partner/coaches/:id and POST /partner/jobs/:jobId/offers return 500. The worked example on this page cannot get past step 3 until that's fixed.

Also open: scopes and the rate limit aren't enforced; expiresAt on an offer is accepted but never stored (offers instead auto-expire 7 days after creation, with no webhook); a partner offer never produces a payout line, so feeBps and the payment model never apply; and the payment.* webhooks have no emitter. Job create/read/update/cancel/complete and the four offer webhooks do work.

Overview

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.

  1. You create a job

    Title, compensation, location, schedule window, and the credentials the worker must hold.

  2. 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 credentialDefinitionIds to filter on the exact credential you require.

  3. You send offers and negotiate

    Offer, counter, withdraw — all from your own system. You can set an expiresAt on an offer and we store it, but note that nothing expires it for you today; see Events to you.

  4. 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.

We render it

Hosted

Not built yet. Documented so you can tell us whether this is the shape you want — partner demand is what schedules it.

Hosted

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.


Our UI in your page

Embedded / SDK

Also not built yet.

Embedded / SDK

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.


You render it

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.

API

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
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.

API

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

cURL
# 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.

API

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.


Examples

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.

Example A · API

Staff your events from your own admin

In development

Nothing to do with registration. If you already run your own platform and just need cleared people, this is a pure server-to-server integration.

https://admin.riversidesports.com/staffing
Your admin, your table. Nothing is outlined in pink because we render none of it — the jobs, candidates and offer states behind the rows are ours, the interface is entirely yours.
You build
Your entire staffing UI
Rough estimate
A sprint
Auth
Partner API key
  1. Get a key and the scopes you need

    jobs:write, coaches:read and offers:write. Ask for those three rather than * — see the note on enforcement.

  2. Create the job with the credentials it requires

    See the job payload above.

  3. 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"
      }'
  4. Take webhooks instead of polling

    offer.accepted, offer.declined, offer.countered and offer.withdrawn all fire today, so your table stays current without a cron. The payment.* events do not fire yet — if you need payout state, ask us rather than waiting on a delivery that won't arrive.

  5. Complete the job when it's done

    POST /partner/jobs/:id/complete moves the job to filled. 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.

Ready to build?

Getting started tells you exactly what to ask us for — and there's a copy-paste email at the bottom of it. Or just write to us: an organization ID, an API key, a staging event to test against, or something still marked Preview or Planned.

Contact us