Skip to main content

Batch Operations

Batch endpoints let you perform multiple operations in a single API call. Use these when you need to process a training week, onboard a team, or import historical activities. The examples read your key from a SATURDAY_API_KEY environment variable, as in Quickstart. Sandbox keys are issued with a separate base URL; using one against api.saturday.fit returns 401 invalid_api_key.

Batch calculate

Calculate prescriptions for multiple scenarios at once. Ideal for building “training week” views or “what if” comparisons.
Each scenario is a full calculate request, so athlete_id goes on the scenario, not at the top level. Successful results come back in request order, with failures omitted. Use the failed indexes in errors to match each result to its original scenario.

Response format

Counts and the request ID sit at the top level. Each entry in results is the same body the single calculate endpoint returns, so it carries tier, safety, and attribution and does not carry an index. errors appears only when at least one scenario failed.
Failed scenarios appear in errors as { "index", "code", "message" }, and the successful ones are still returned: batch operations do not fail atomically. Because failures are dropped from results rather than nulled, results is shorter than scenarios when anything fails, and a result’s position no longer matches its scenario index. When you need that mapping and failures are possible, read the failed indexes from errors first and reconstruct alignment from them.

Limits

Exceeding 50 returns 400 with the code batch_too_large, and an empty scenarios array returns 400 with empty_batch.

Sizing the wait

A batch takes as long as the sum of its scenarios. Two things let you show real progress instead of a spinner: the response carries an X-Batch-Estimated-Ms header, flushed before processing begins, and sending "estimate_only": true returns the same estimate immediately without running any calculations, consuming quota, or returning results.

Bulk athlete create

Onboard multiple athletes in one call. Useful for team imports or platform migrations.
This endpoint takes a partner API key only. An athlete-delegated OAuth token is rejected up front with 403 and code athlete_scope rather than partway through, so a scope mistake never leaves some athletes created and others not.

Limits

Each athlete is also validated on weight_kg (a negative value is rejected), year_of_birth (1900 to the current year), and sex (male, female, or intersex). A violation fails that item only, with code invalid_value; an item naming none of name, email, or external_id fails with missing_identifier. A year_of_birth that makes the athlete younger than 14 (the current year minus the birth year) fails that item with under_minimum_age, and the item’s message names the minimum age. The batch is also checked against your account’s total athlete quota before processing. If you are at the cap the whole request returns 403 with the code resource_limit_exceeded, rather than partially filling.
external_id is not deduplicated. Saturday does not reject or merge an athlete whose external_id you used before; every create mints a new athlete with a new UUID. Retrying a batch that partially succeeded will therefore create duplicates of the athletes that succeeded the first time. Track the returned IDs against your external_id values and retry only the items that failed.

Activity import

Import multiple activities for an athlete at once. Useful for backfilling historical data from other platforms.
An imported activity accepts these fields and no others. Unknown keys are ignored silently, so check this list rather than assuming a field was stored:
Imported activities are stamped with the time Saturday created them, so a backfill does not preserve the original activity dates. Keep a date mapping keyed by external_id if you need it.
Up to 200 activities per import. Exceeding that returns 400 with batch_too_large. The import is also checked against the athlete’s activity quota before processing, returning 403 with resource_limit_exceeded if it would exceed the cap. Per-item validation fails an item with missing_field when type is absent and invalid_value for an unknown type or a non-positive duration_min.

Calculating prescriptions during import

Set calculate: true at the top level to calculate a prescription for every imported activity, or on individual activities to calculate selectively:
Each calculation runs the same tier-aware path as the single calculate endpoint: full-tier and in-trial athletes get exact prescriptions, stored on the activity and mirrored on imported[].prescription, while teaser-tier athletes get per-hour ranges with a subscription_cta. Trial athletes debit their daily call allowance per calculated activity, so a large import can exhaust the day’s allowance mid-batch, after which the remaining items return teaser ranges. Per-item outcomes ride a prescriptions array in the response, each with index, activity_id, and the tier-aware result. A failed calculation never fails the import: the activity is still created, and the failure appears on that item’s code and message. Imports count against rate limits per item, like batch calculate.

Error handling in batch operations

Batch operations use partial success semantics. If 3 of 5 items succeed and 2 fail, the 3 successes are committed and the 2 failures are returned in errors. The HTTP status is 200 whenever the request itself was accepted, even when every item failed; a 400 or 403 means nothing was processed. Every batch response uses the same envelope. The success array is named for the operation (results, created, or imported), and alongside it sit total, succeeded, failed, request_id, and, when anything failed, errors. Each error is a flat object with index, code, and message:
Read both arrays. index on an error refers to the item’s position in your request, which is the only reliable way to tell which input failed. Unlike the resource IDs above, request_id is not a UUID: it is req_ followed by 12 hex characters. Log it. It is what support needs to trace a specific call.