Guides

Logging workouts

Send each workout once it is finished. One call records it, turns it into impact and tells you the result.

Sending a workout#

curl -X POST "https://dyb5ahy4siv91.cloudfront.net/partner/v1/activities" \
  -H "Authorization: Bearer $AG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "partnerUserId": "emp-1042",
  "externalId": "wk-20261014-0007",
  "type": "RUNNING",
  "startedAt": "2026-10-14T05:30:00Z",
  "durationSeconds": 2700,
  "distanceMeters": 8200
}'
userIdstringsometimes required

Our user ID, the userId from Add a person. Send exactly one of userId, userEmail and partnerUserId.

userEmailstringsometimes required

The email you added the person with.

partnerUserIdstringsometimes required

Your own ID for the person, as you sent it in Add a person.

externalIdstringrequired

Your ID for this workout, up to 128 characters: letters, numbers and - _ . :. It never changes and is also the retry key: the same workout sent twice counts once.

typestringrequired

A key from activityTypes in Get your team.

startedAtISO 8601required

From the start of yesterday, in your team's time zone, until now. The workout must end no more than 5 minutes in the future.

durationSecondsintegerrequired

60 to 86,400.

distanceMetersintegersometimes required

Required when the type has earnsBy: "DISTANCE". Above 0, up to 1,000,000.

kilocaloriesintegersometimes required

Required, above 0, when the type has earnsBy: "CALORIES". Up to 20,000.

  • Send exactly one of userId, userEmail and partnerUserId to say whose workout it is.
  • We take workouts from the start of yesterday, in your team's time zone, until now. Others get 422 started_at_out_of_range.
  • Each type earns by distance or by calories (earnsBy in Get your team). Send distanceMeters or kilocalories to match.
  • We do not take heart rate, elevation or GPS routes. They do not change a workout's impact, and they are health and location data we do not want. Any field not listed above gets 400 unknown_field.

Counted or refused#

Every workout gets a response straight away. A 201 means it counted: status is COUNTED and impact says what it earned. A workout that cannot count is refused with an error and not stored, so you can send it again later if the reason goes away.

409 Conflict
{
  "error": {
    "code": "daily_limit_reached",
    "message": "The person's daily limit is reached. It resets at error.resetsAt.",
    "fields": [],
    "resetsAt": "2026-10-14T22:00:00Z"
  },
  "requestId": "c4c1c7eb-1eef-45c5-9c4b-a5febef121d5"
}
codeMeaning
409 daily_limit_reachedThe person reached today's limit. error.resetsAt says when it resets (UTC).
409 overlapping_activityThe person already has a workout at the same time, for example the same run from Strava or Apple Health. It counts once.

A counted workout can still earn less than usual, when only part of it fits under a cap. impact.reducedBy then says which:

reducedByMeaning
ACTIVITY_CAPOne workout can earn only so much impact.
TEAM_GOALOnly part of it fit before the team goal.
DAILY_IMPACTOnly part of it fit in the person's daily impact limit.

Retries and duplicates#

Your externalId is the retry key. Sending the same workout again with the same externalId and the same values returns the stored workout with 200 and counts it once. So when a call times out or fails with a 5xx, just send it again.

  • Same externalId with different values gets 409 duplicate_external_id. A workout cannot be changed once sent.
  • If the person also tracks with Strava or Apple Health, the same workout can reach us twice. The second one is refused with 409 overlapping_activity and counts once.
  • Sent the wrong thing? There is no update or delete yet. Contact us with the request ID and we fix it.

Limits#

Workouts follow your team's rules: a daily limit per person, the team's impact goal and the sponsor's budget. Read them, along with places left and the workout types your team takes, from Get your team.

Show refused workouts to your people too, with the reason. “You reached today's limit” is better than a workout that silently earned nothing.