How do you integrate a coaching API into a fitness app?
Short answer: server to server, from the backend your client already syncs health data into. Not from the mobile app. That constraint is not ours, it is the platforms’: neither Apple HealthKit nor Android Health Connect has a server-side API, so a backend that receives uploaded health data is already the shape of every serious fitness app. Coaching belongs in that same backend. Four steps below, with the real headers, the real rate limits and the full failure table.
There is no cloud to read your users’ health data from.
This is the single fact that decides your architecture, and it surprises most teams once. Health Connect stores data on the device, encrypted, not against a Google account, and Google publishes no server-side API for it: an Android app must read locally and upload. HealthKit is the same story on iOS, where the documented path is an observer query plus background delivery in your own app, then an upload to your own endpoint. There is no token you can present to Apple or Google from a server to fetch a user’s history.
So you own a sync endpoint whether you wanted to or not. The good news: once that exists, adding coaching is one authenticated call from code you already run, against data already in your database. The bad news for anyone hoping to skip the backend: there is no client-only version of this, with or without us.
| Layer | Who owns it | Why it lands there |
|---|---|---|
| Device sensors and health store | Apple / Google, read by your app | On-device by design, permission-gated per data type. |
| Sync and storage | You | The only path off the device is your app uploading to your backend. |
| Wearable and third-party pipes | A data API, if you use one | Server-side webhooks for devices outside the phone. Complementary to this layer, not a substitute. See CoachLayer vs wearable data APIs. |
| Coaching decisions | CoachLayer | One key, called from your backend with the context you already hold. Nothing to sync, nothing to link. |
| Presentation | You | Your app renders the brief, the chat reply, the report. Your product, your voice. |
Four steps, in the order that de-risks fastest.
- Get a key without a meeting. Self-serve signup returns a test key once, stored on our side only as a hash, with 300 free credits a month and no card. Put it in your server secret store, not in your mobile bundle and not in a client-side environment variable.
- Prove one endpoint against your real data shapes. Start with the readiness brief at 3 credits or coach chat at 2 credits per message: cheap, interactive, and the two your users notice. Send a real athlete’s context, not a fixture. The whole point of a sandbox is finding out that your sleep field is a string before launch week.
- Wire metering into your own ledger on day one. Persist
X-Request-IdandX-Credits-Chargednext to your own event row. Retrofitting this after a surprising invoice is miserable; doing it during the first integration costs one column. - Handle the failure table before you ship, not after. Every error in the API shares one envelope, so this is a single handler and a single test, not a per-endpoint chore. The table is below.
Sequencing matters more than speed. Teams that do step 3 last are the ones who spend a Friday reconciling. The quickstart with copy-paste requests for every endpoint lives in the docs, and the whole reference is available as one file for a coding agent if you would rather have your assistant write the client.
Send context per call. Do not build a sync.
There is no onboarding handshake, no account linking and no copy of your user table on our side. Each call carries the context that call needs, in the request body, and the response comes back structured enough to render directly. That keeps three things true at once: your data stays in your database, your privacy story stays yours to tell, and you can change what you send without a migration on either side.
Practically, the athlete context worth sending is the context a human coach would ask for: the last few sessions, the current program, the stated goal, and whatever recovery signal you actually have. Missing signals degrade the answer, they do not break the call, which matters a lot when a user denied a health permission. If you have nothing but a workout log, start there and add signals as your sync improves.
Rate limits are per tier, and per minute.
Interactive endpoints are the ones that need headroom; scheduled ones do not. 10 requests per minute on the sandbox is enough to build against and not enough to launch on. Launch gives you 60 and Growth gives you 300. If your weekly reports all fire at the same Sunday timestamp, queue them and spread them: a batch job racing an interactive chat endpoint for the same per-minute budget is the most common self-inflicted 429 we see. Exact per-tier numbers and overage rates are on the pricing table, and the calculator reads the same config the billing engine does.
One error envelope. Seven things that can go wrong.
Every failure returns {"error": {"type", "code", "message", "request_id"}}, so you write the handler once. Log request_id on every branch; it is the only thing either side needs to find a specific call.
| Status | Type | What happened | What your code should do |
|---|---|---|---|
401 | authentication_error | The key is missing, malformed or revoked. | Fix the Bearer header. Never retry a 401 in a loop; it will not start working. |
400 | invalid_request_error | The body failed validation before any model ran. | Log the request_id and the field. Not charged, so no credit reconciliation needed. |
402 | billing_error (credit_limit_exhausted) | The billing period allowance is spent and overage is not open. | Degrade the feature gracefully in your app. This is the one to design a fallback for. |
403 | billing_error (endpoint_not_in_tier) | The tier does not include that endpoint. | A deploy-time problem, not a runtime one. Assert your tier covers your call graph in CI. |
429 | rate_limit_error | Requests per minute exceeded for the tier. | Honour Retry-After. Queue non-interactive work such as weekly reports instead of retrying inline. |
502 | upstream_error | The coaching backend failed on this call. | Never charged. Safe to retry once with backoff. |
503 | capacity_error | free_tier_paused (shared sandbox budget spent for the month) or gateway_unavailable (transient). | Retry shortly. Paid tiers are unaffected by free_tier_paused, which is the reason to leave the sandbox before launch. |
Two of these are worth a product decision rather than a code path. A 402 is your feature going dark for a paying user, so decide now whether that degrades to a cached brief, a generic message or a hidden tab. And free_tier_paused exists because the free sandbox runs on a shared monthly budget: it is a build-time convenience, never a launch plan.
Before you open an editor.
Can I call a coaching API directly from my mobile app?
No. The key is a server credential: shipping it in a mobile binary means anyone who unzips your app can spend your credits. Call CoachLayer from your backend and let your app talk only to your backend. You almost certainly already have that backend, because neither HealthKit nor Health Connect exposes a server-side API, so your client is already uploading health data to you.
How long does the integration actually take?
The first authenticated call takes minutes: self-serve signup issues a test key with no card, and every endpoint takes JSON you already hold. The real work is not the HTTP call, it is deciding which athlete context you send and where the responses are cached. Teams ship one endpoint in an afternoon and the rest over a sprint.
What athlete data do I have to send, and what do you store?
You send only the context the endpoint needs, passed in the request body: recent sessions, the current program, sleep and load signals where you have them. There is no onboarding sync, no account linking and no long-lived copy of your user table on our side. Requests are metered in the usage ledger by request id, not by personal profile.
How do I meter and reconcile credits from my side?
Every metered response carries X-Credits-Charged, X-Credits-Overage, X-Credits-Remaining and X-Request-Id. Persist the request id and the charge next to your own event, and your ledger reconciles against ours line by line without a support ticket. Rate limits are per tier: 10 requests per minute on the sandbox, 60 on Launch, 300 on Growth.
What breaks in production, and how should I handle it?
Five failure modes matter: 402 when the allowance is spent (design a graceful degrade), 429 when you exceed the tier rate limit (honour Retry-After and queue batch work), 502 upstream failures (never charged, retry once with backoff), 503 capacity (retry shortly), and 403 endpoint-not-in-tier, which is a deploy-time bug you should assert against in CI rather than discover at runtime.
Where the platform claims come from.
The on-device constraint is the load-bearing claim on this page, so here is where to check it rather than taking our word: Google’s Health Connect developer documentation for the client-only data model, and Apple’s HealthKit documentation for the on-device store and the observer-query plus background-delivery sync path. Checked September 2026. Credit weights, rate limits and error types on this page are generated from the same configuration the gateway bills from, and the contract itself is published as OpenAPI 3.1.
Start with the sandbox, not a sales call.
300 credits a month, no card, all eight endpoints. Enough to integrate one properly before you talk to anyone.
Get a sandbox key