Developers
Open any workout in SyncroFit
Hand off a workout to SyncroFit from any iOS app or website with a single URL. Users tap the link, SyncroFit opens to a preview, and one more tap adds the workout to their library ready to run. No SDK to install. No API key required.
The trainee's imported workout appears in SyncroFit's Workouts tab + at mysyncrofit.com/workouts tagged with your branding from from.name, their mode badge, and any equipment tags — so workouts you share via deep link get found organically when other users browse the directory. Webhook events let you mirror import + completion activity back to your backend without polling.
Looking for every field you can send? The field reference → lists every circuit and exercise option with types, defaults, and allowed values.
Live integrations
Apps already handing workouts to SyncroFit with the contract below — a working reference for what you're building.
- Vitality Tracker
A workout-logging web app. Build a routine in Vitality, then hand it to SyncroFit in one tap — the exercises, sets, reps, rest, and images ride across a syncrofit:// link and land ready to run.
The two link formats
Both URLs accept the same parameters and produce the same result. Pick whichever fits your platform.
Universal Link
https://www.mysyncrofit.com/run?…Use from anywhere — websites, emails, push notification payloads. iOS opens the SyncroFit app if installed, otherwise renders a web preview with an App Store CTA. Recommended default.
URL Scheme
syncrofit://run?…Use from iOS apps where you can detect SyncroFit being installed via UIApplication.canOpenURL. Faster to launch than a Universal Link. No web fallback — the link only works if SyncroFit is installed.
Format 1 — Inline parameters
Easiest format. Single-exercise circuit defined entirely in the query string. Use this for quick one-off links.
Universal Link
https://www.mysyncrofit.com/run
?name=Quick%20HIIT
&sets=4
&reps=8
&action=30
&rest=10
&from=Coach%20SmithParameters
| Parameter | Type | Description | Default |
|---|---|---|---|
| name | string | Workout name shown in the import preview | — |
| action | integer | Seconds of work per rep | 30 |
| rest | integer | Seconds of rest between reps | 10 |
| sets | integer | Number of sets | 1 |
| reps | integer | Reps per set | 8 |
| betweenSetRest | integer | Extra rest after each set | 0 |
| notes | string | Coaching notes for this exercise | |
| from | string | Creator attribution (your app or coach name) | |
| id | string | Stable identifier you control. Re-importing the same id refreshes the existing workout in the user's library instead of duplicating it. | |
| webhook | https URL | HTTPS endpoint where SyncroFit POSTs import + completion events for this circuit. Opt-in by the user. | |
| mode | string | DEFAULT workout mode for exercises that don't specify their own (Phase 2 per-exercise mode rollout): 'intervals', 'forTime', 'amrap', or 'emom'. Each exercise can override. | intervals |
| amrapMinutes | integer | DEFAULT AMRAP cap (minutes) for exercises with mode='amrap' that don't specify their own. Clamped 1-60. | 12 |
| emomMinutes | integer | DEFAULT EMOM cap (minutes) for exercises with mode='emom' that don't specify their own. Clamped 1-60. | 12 |
| reqEquip | csv | Comma-separated required equipment. Drives the equipment filter on /workouts. | |
| optEquip | csv | Comma-separated optional equipment. 'Nice to have'; doesn't gate search. | |
| actionImage | https URL | Background photo or GIF during the action (work) phase | |
| restImage | https URL | Background photo or GIF during the rest phase | |
| coachVoice | https URL | Coach voice memo (M4A/MP3) that plays when the exercise begins |
Deduplication — the id field
Re-importing the same partner workout into a user's library doesn't create duplicates. SyncroFit gives each partner-supplied circuit a stable identity key — the next time a user opens a link with the same key, the import flow updates the existing workout in place instead of stacking a copy alongside it. The import preview surfaces the refresh: a yellow "Will refresh: Workout Name" banner appears at the top of the sheet, and the action button reads Refresh instead of Import.
How identity is composed (priority order)
- Partner-supplied
id(recommended). When the JSON or inline params includeid, that's the identity. Pick a value that won't change across edits of the same workout (e.g.coach-smith-lower-body-strength-v1). Editing other fields and re-issuing the link will refresh existing users' libraries. - Remote URL (Format 3 —
?from=…). Without an explicitid, the URL itself becomes the identity. Same URL → same workout in the user's library. You can edit the JSON at that URL and existing users get the new version on next open. - Content hash fallback. Without
idAND without a remote URL, SyncroFit hashes the workout's name + exercise definitions. Structurally identical payloads dedupe; partner edits to timing, exercise names, or counts produce a new identity and a new workout (not a refresh). Partners that want refresh semantics on edits should always supply an explicitid.
The dedup key is scoped by sender — two partners can use the same id value without colliding. (Internally: partner:<sender>:id:<id>.)
On refresh, the existing workout's exercises, phase images, and coach voice memos are replaced wholesale. Local edits the user made to that workout (e.g. adding their own notes or images) will be overwritten. Local UUID, created-at timestamp, and publish state are preserved so any share links the user generated to their copy continue to work.
Webhooks — import + completion events
Add a single webhook URL to your circuit and SyncroFit POSTs JSON event payloads when a user imports the workout and again when they complete it. Lets you mirror the user's SyncroFit activity into your own backend without polling.
Privacy gating
- Opt-in. Default OFF. The user must flip Settings → Privacy & Safety → Share with third-party app makers before any event fires. No exception.
- Scoped user IDs. The
user.scopedIdfield issha256(traineeUid + partnerKey)[:16]. Same user + same partner = same id across imports, so you can dedupe your own users. Different partner = different id, so you can't correlate users across apps. - Display name only when opted in. Omitted from the payload entirely when the user hasn't set one or hasn't opted in.
Imported event
POST your-webhook-url
Headers:
Content-Type: application/json
X-SyncroFit-Event-Type: circuit.imported
User-Agent: SyncroFit/1.0 (iOS Webhook)
Body:
{
"event": "circuit.imported",
"circuit": {
"id": "coach-smith-morning-hiit-v1",
"name": "Morning HIIT"
},
"user": {
"scopedId": "a3f9c2e8b1d40576",
"displayName": "Dino F"
},
"timestamp": "2026-05-22T03:30:00Z",
"schemaVersion": 1
}Completed event
The result block is mode-aware. durationSeconds is always present (so pre-Phase-3 partners still parse correctly). For amrap and emom workouts the relevant CrossFit-standard score (rounds completed or minutes completed) is included alongside.
POST your-webhook-url — intervals or forTime result
Headers:
Content-Type: application/json
X-SyncroFit-Event-Type: circuit.completed
User-Agent: SyncroFit/1.0 (iOS Webhook)
Body:
{
"event": "circuit.completed",
"circuit": {
"id": "coach-smith-morning-hiit-v1",
"name": "Morning HIIT"
},
"user": {
"scopedId": "a3f9c2e8b1d40576",
"displayName": "Dino F"
},
"startedAt": "2026-05-22T03:30:00Z",
"completedAt": "2026-05-22T03:53:00Z",
"durationSeconds": 1380,
"result": {
"mode": "intervals",
"durationSeconds": 1380
},
"timestamp": "2026-05-22T03:53:00Z",
"schemaVersion": 1
}POST your-webhook-url — amrap result
Body:
{
"event": "circuit.completed",
"circuit": { "id": "amrap-15-pullups-v1", "name": "15-min AMRAP" },
"user": { "scopedId": "a3f9c2e8b1d40576" },
"startedAt": "2026-05-22T03:30:00Z",
"completedAt": "2026-05-22T03:45:00Z",
"durationSeconds": 900,
"result": {
"mode": "amrap",
"durationSeconds": 900,
"amrapRounds": 12
},
"timestamp": "2026-05-22T03:45:00Z",
"schemaVersion": 1
}POST your-webhook-url — emom result
Body:
{
"event": "circuit.completed",
"circuit": { "id": "emom-12-kb-swings-v1", "name": "12-min EMOM" },
"user": { "scopedId": "a3f9c2e8b1d40576" },
"startedAt": "2026-05-22T03:30:00Z",
"completedAt": "2026-05-22T03:42:00Z",
"durationSeconds": 720,
"result": {
"mode": "emom",
"durationSeconds": 720,
"emomMinutesCompleted": 12
},
"timestamp": "2026-05-22T03:42:00Z",
"schemaVersion": 1
}Reliability
- HTTPS only. Plain HTTP URLs are silently dropped (the user's import flow doesn't see an error — your event just doesn't fire).
- 5-second timeout. A slow endpoint never blocks the import sheet or the completion sound. If your server can't respond in 5 seconds, queue the work and return 200 immediately.
- Fire-and-forget. Response codes are logged client-side but no retry happens in v1.0. Anything in the 2xx range is success; anything else is a delivery failure on your end. v1.1 will add an exponential-backoff retry queue.
- Verify the source. Anyone can POST to your webhook URL. For untrusted-environment use, add a secret to your URL path (e.g.
https://api.partner.com/syncrofit-events?secret=…) and validate on receipt.
Media URLs — images + coach voice
Every exercise can carry partner-hosted media. SyncroFit downloads each asset on import, persists it locally to the user's device, and the imported workout looks identical to the one published in your app. Same fields exist in inline-params (single exercise) and JSON (multi-exercise) formats.
actionImageURL
Background shown during the work phase of the timer. Replaces the default gradient.
restImageURL
Background shown during the rest phase. Often a calmer variant of the action image.
coachVoiceURL
Plays once when the exercise becomes active. Your coach's briefing, no need for the user to read the notes.
mode
PER-EXERCISE workout mode. Lets you mix patterns within one workout — e.g. intervals strength sets followed by a forTime metcon followed by an amrap finisher. Falls back to the top-level mode field when this exercise doesn't set its own. Values: intervals | forTime | amrap | emom.
amrapMinutes
AMRAP time cap (minutes) for THIS exercise. Only meaningful when its mode is amrap. Clamped to [1, 60]; falls back to the top-level amrapMinutes when missing.
emomMinutes
EMOM time cap (minutes) for THIS exercise. Same shape as amrapMinutes, only meaningful when this exercise's mode is emom.
Requirements
- https:// only — plain HTTP URLs are silently dropped on iOS and rejected as mixed content on the web preview.
- Per-asset cap: 10 MB. Larger payloads are dropped without failing the import.
- Images:
image/jpeg,image/png, orimage/gif(animated GIFs preserved as GIFs). - Audio:
audio/m4aoraudio/mpeg(MP3). - 10-second timeout per asset. Slow CDN endpoints get dropped so the import sheet always opens promptly.
- Per-asset failures (missing URL, 404, wrong content-type) don't break the import — the workout just lands without that asset.
Workout modes — including mixed-mode workouts
SyncroFit ships four workout modes. Each exercise in a circuit can use its own mode — the runtime switches behavior automatically as it crosses preset boundaries. So a single circuit can blend a strength block, a metcon, and a finisher without forcing your athletes to navigate between three separate workouts.
intervals
Classic phase-based timing: sets × reps × action/rest cycles. The runtime drives transitions; voice cues call every phase. Default for every exercise.
forTime
Stopwatch counts UP from 0. Athlete races through the prescribed work and taps Done to advance to the next preset. Elapsed time IS the result.
amrap
Countdown from amrapMinutes. Athlete repeats the prescribed work as a "round" until the cap expires. Score = rounds completed. Default cap 12 min.
emom
Every minute on the minute: bell rings, athlete does the prescribed work, rests the remainder. Runs for emomMinutes total. Score = minutes completed. Default cap 12 min.
Mixed-mode example
A typical CrossFit programming pattern — strength block followed by a metcon followed by a finisher — expressed as a single circuit:
Mixed-mode circuit JSON
{
"name": "Strength + metcon + finisher",
"exercises": [
{ "name": "Back squat", "sets": 5, "reps": 5, "actionTime": 30, "restTime": 0,
"betweenSetRest": 120, "mode": "intervals",
"requiredEquipment": ["Barbell"] },
{ "name": "50 burpees for time", "sets": 1, "reps": 50, "actionTime": 60, "restTime": 0,
"mode": "forTime" },
{ "name": "12-min AMRAP — 10 pull-ups + 15 push-ups",
"sets": 1, "reps": 25, "actionTime": 30, "restTime": 0,
"mode": "amrap", "amrapMinutes": 12,
"requiredEquipment": ["Pull-up bar"] }
]
}Mixed-mode circuits get a MIXED badge in the SyncroFit Workouts directory + on mysyncrofit.com/workouts. Uniform-mode circuits get the specific mode badge with the cap baked in (e.g. AMRAP 12).
The mode field on each exercise is optional — when missing, the exercise inherits the top-level circuit mode field. So legacy uniform-mode payloads (one circuit-level mode, no per-exercise overrides) keep working unchanged.
Equipment filter — get your workouts found
Tag each exercise with the equipment it requires. Trainees on SyncroFit pick chips for what they have on hand; the /workouts directory shows only circuits whose every required item is in their selection. Untagged exercises are treated as bodyweight-only and always appear.
requiredEquipment
Gates the filter. A trainee who hasn't ticked "Dumbbells" doesn't see your dumbbell-required circuit. String array; values should come from SyncroFit's canonical taxonomy (see below) but anything goes — strict-string matches are case-sensitive.
optionalEquipment
"Nice to have." Shown on the workout detail page as a hint; doesn't gate search. Use this for accessories that improve the exercise but aren't strictly required (e.g. yoga mat for floor work).
Canonical taxonomy (case-sensitive): No equipment, Dumbbells, Kettlebell, Barbell, Resistance bands, Pull-up bar, Yoga mat, Bench, Stability ball, Medicine ball, Jump rope, Foam roller, TRX / suspension, Boxing gloves, Treadmill, Stationary bike, Rowing machine. Custom values are accepted but won't match the trainee's chip selection — pick from the canonical list when possible.
Format 2 — Inline JSON
Full circuit definition encoded as a JSON object and URL-encoded into the circuit query parameter. Use this when you have a multi-exercise workout to share.
JSON schema
{
"id": "coach-smith-lower-body-strength-v1",
"name": "Lower body strength",
"description": "Optional explainer text",
"from": {
"name": "Coach Smith",
"organization": "Smith Fitness"
},
"restBetweenExercises": 30,
"webhook": "https://api.partner.com/syncrofit-events",
"mode": "intervals",
"exercises": [
{
"name": "Goblet squats",
"notes": "Keep chest up, drive through heels.",
"sets": 4,
"reps": 8,
"actionTime": 30,
"restTime": 10,
"betweenSetRest": 60,
"actionImageURL": "https://cdn.partner.com/goblet-squat-action.jpg",
"restImageURL": "https://cdn.partner.com/goblet-squat-rest.jpg",
"coachVoiceURL": "https://cdn.partner.com/goblet-squat-cue.m4a",
"requiredEquipment": ["Dumbbells"],
"optionalEquipment": ["Yoga mat"],
"mode": "intervals"
},
{
"name": "Burpees for time",
"sets": 1,
"reps": 50,
"actionTime": 60,
"restTime": 0,
"mode": "forTime",
"requiredEquipment": []
},
{
"name": "Pull-up AMRAP",
"sets": 1,
"reps": 10,
"actionTime": 30,
"restTime": 0,
"mode": "amrap",
"amrapMinutes": 5,
"requiredEquipment": ["Pull-up bar"]
}
]
}Universal Link
https://www.mysyncrofit.com/run?circuit=<url-encoded-json>In Node: encodeURIComponent(JSON.stringify(payload)). Keep the encoded result under ~6 KB so the URL stays under common platform limits (iMessage truncates around 8 KB).
Format 3 — Remote URL
SyncroFit fetches the circuit definition from a URL you host. Lets you edit the workout server-side without reissuing share links to your users. The fetched body is the same JSON schema as Format 2.
Universal Link
https://www.mysyncrofit.com/run?from=https%3A%2F%2Fpartner.com%2Fworkouts%2F123.jsonRequirements
- URL must be
https://(plain HTTP rejected — no SSR fetches on the user's behalf over unencrypted transport) - Response must be
Content-Type: application/json - Response body capped at 256 KB
- 10-second fetch timeout — slower endpoints are dropped
- CORS not required (fetched server-side from iOS or via SSR on the web)
iOS detection example
From inside an iOS app, you can check whether SyncroFit is installed before constructing the link, and fall back to the Universal Link otherwise.
Swift
import UIKit
func openInSyncroFit(name: String, action: Int, rest: Int, sets: Int, reps: Int) {
let params = "name=\(name.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!)" +
"&action=\(action)&rest=\(rest)&sets=\(sets)&reps=\(reps)"
let urlScheme = URL(string: "syncrofit://run?\(params)")!
let universal = URL(string: "https://www.mysyncrofit.com/run?\(params)")!
// Try the URL scheme first — instantaneous when the app is installed.
// Fall back to the Universal Link, which iOS handles by either
// launching SyncroFit or opening the web fallback in Safari.
if UIApplication.shared.canOpenURL(urlScheme) {
UIApplication.shared.open(urlScheme)
} else {
UIApplication.shared.open(universal)
}
}Web link example
From a website or email, just emit a regular <a> tag pointing at the Universal Link. iOS Safari + iMessage + Mail + every major chat app honor it.
HTML
<a href="https://www.mysyncrofit.com/run?name=Quick%20HIIT&sets=4&reps=8&action=30&rest=10">
Run this workout in SyncroFit
</a>Try it now
Tap any link below on iOS to test the flow. If SyncroFit is installed, the app opens to the import sheet. Otherwise the /run page's preview renders inside Safari. Each example uses a different mode so you can see how the run view changes.
Questions
Email developers@mysyncrofit.com. API is stable from v1.0 onward — any breaking changes get a versioned path (/v2/run) with at least 6 months of overlap.