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%20Smith

Parameters

ParameterTypeDescriptionDefault
namestringWorkout name shown in the import preview
actionintegerSeconds of work per rep30
restintegerSeconds of rest between reps10
setsintegerNumber of sets1
repsintegerReps per set8
betweenSetRestintegerExtra rest after each set0
notesstringCoaching notes for this exercise
fromstringCreator attribution (your app or coach name)
idstringStable identifier you control. Re-importing the same id refreshes the existing workout in the user's library instead of duplicating it.
webhookhttps URLHTTPS endpoint where SyncroFit POSTs import + completion events for this circuit. Opt-in by the user.
modestringDEFAULT 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
amrapMinutesintegerDEFAULT AMRAP cap (minutes) for exercises with mode='amrap' that don't specify their own. Clamped 1-60.12
emomMinutesintegerDEFAULT EMOM cap (minutes) for exercises with mode='emom' that don't specify their own. Clamped 1-60.12
reqEquipcsvComma-separated required equipment. Drives the equipment filter on /workouts.
optEquipcsvComma-separated optional equipment. 'Nice to have'; doesn't gate search.
actionImagehttps URLBackground photo or GIF during the action (work) phase
restImagehttps URLBackground photo or GIF during the rest phase
coachVoicehttps URLCoach 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)

  1. Partner-supplied id (recommended). When the JSON or inline params include id, 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.
  2. Remote URL (Format 3 — ?from=…). Without an explicit id, 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.
  3. Content hash fallback. Without id AND 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 explicit id.

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.scopedId field is sha256(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, or image/gif (animated GIFs preserved as GIFs).
  • Audio: audio/m4a or audio/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.json

Requirements

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