Partner integration with Ivolution Analytics
Ivolution partners can read the athletes, athlete profiles and results of the institutions that authorize them, and display their dashboards inside their own product.
Before you start
This API is read-only. It exposes the data an institution produced in Ivolution and authorized to share.
Every route lives under /api/v1/partner/. The institution is not part of the path: it is given with the institution_id query parameter on every data call. Every response, successful or not, comes in the same envelope: status, data, message, errors and meta.
The full reference, generated from the OpenAPI specification, is at /api/v1/partner/docs and lets you try every operation from the browser.
Out of scope:
The model in four words
The whole system is explained with four terms. Read them before any endpoint.
| Term | What it is |
|---|---|
| Partner | The organization with a current commercial agreement. It is an entity of the system, not a person or an institution. |
| Credential | What the partner presents to authenticate. It belongs to the partner, not to an integration: the same credential works for all of them. It says who is calling. |
| Integration | The link that one institution authorizes with a partner. It defines what can be read, and it is the unit access is audited on. Without an active integration, there is no data. |
| Link code | The single-use code the institution gets when it authorizes and hands over to the partner. It is redeemed once and expires after 48 h. It is not a credential and grants no access on its own. |
A valid credential is not enough to read anything. Authentication and scope are different things: the credential identifies the partner, the integration determines which institutions authorized it.
Authentication
Every call carries the partner credential in the Authorization: Bearer header. The credential has the form <prefix>.<secret>, and the prefix states the environment: a production credential starts with ivo_live_ and is rejected in any other environment, just as a test credential is rejected in production.
To rotate it, Ivolution issues a new credential and both work at the same time until the partner's system has finished switching over; only then is the old one revoked, with no service interruption.
The credential also defines the scopes: athletes for athletes and athlete profiles, results for results and dashboards for the catalog and the dashboard URLs. If the one a route requires is missing, the response is 403 insufficient_scope.
curl "https://app.analytics-api.ivolution.com.ar/api/v1/partner/institutions" \
-H "Authorization: Bearer $IVOLUTION_PARTNER_KEY" \
-H "Accept: application/json"{
"status": "success",
"data": [
{
"institution_id": 42,
"name": "Club Deportivo Ejemplo",
"authorized_at": "2026-03-12T14:02:00Z"
}
],
"message": null,
"errors": null,
"meta": {
"pagination": null,
"cursor": { "next_cursor": null, "has_more": false }
}
}The list shows only the institutions with an active integration. It does not depend on the subscription: a listed institution may answer subscription_required on the next data call.
Linking, step by step
Linking turns a commercial agreement into technical permission over one specific institution. It takes four steps, and only two of them are the partner's.
Authorizes from Analytics
An administrator of the institution goes to Settings → Integrations and authorizes the partner. The system then generates a link code and shows it only once.
Hands the code to the partner
Through whatever channel both parties agree on. The code expires after 48 h if it is not redeemed, and it gives access to nothing without the partner's credential.
Redeems the code
A single call to POST /api/v1/partner/link, with the partner credential. The code is consumed when redeemed: a second attempt answers 409 link_code_invalid.
Is cleared to read
The integration becomes active, the response carries the institution_id and the institution shows up in the list. Only then do the data resources respond.
curl -X POST "https://app.analytics-api.ivolution.com.ar/api/v1/partner/link" \
-H "Authorization: Bearer $IVOLUTION_PARTNER_KEY" \
-H "Content-Type: application/json" \
-d '{"code": "EjemploDeCodigoDeVinculacion-NoEsReal_00000"}'{
"status": "success",
"data": {
"institution_id": 42,
"name": "Club Deportivo Ejemplo",
"authorized_at": "2026-03-12T14:02:00Z"
},
"message": null,
"errors": null,
"meta": null
}The code is a 43-character string. Any redemption failure —a code that is unknown, issued for another partner, already redeemed or expired— answers the same 409 link_code_invalid, without saying which.
Redemption cannot be undone. Keep the institution_id it returns: it is the query parameter of every data call. If the code is lost before it is redeemed, the institution must revoke the integration and authorize again.
Integration states
Only one of the four allows reading. The other three return an error, and the partner's system must tell them apart to show a useful message.
| State | What it means | Can read? |
|---|---|---|
pending | The institution authorized, but the code has not been redeemed yet. | No: 409 integration_pending |
active | Redeemed. The only state that grants access. | Yes |
expired | The code expired without being redeemed. Terminal. | No: 409 integration_expired |
revoked | The institution cut off access. Terminal. | No: 403 integration_revoked |
expired and revoked are terminal, but they do not block a new authorization: the institution can authorize again whenever it wants, which generates another code. With no integration at all with that institution, the response is 404 integration_not_found.
To check an integration without reading data, use GET /api/v1/partner/integration, with the same institution_id: it answers 200 only if the integration is active and the institution has a current subscription or trial.
Athletes and email matching
The list returns the institution's athletes, active or not, sorted by id. Each athlete carries id, active, name, last_name, email, sex and birthdate; the last three may come as null.
curl "https://app.analytics-api.ivolution.com.ar/api/v1/partner/athletes?institution_id=42" \
-H "Authorization: Bearer $IVOLUTION_PARTNER_KEY"{
"status": "success",
"data": [
{
"id": 4821,
"active": true,
"email": "lucia.ejemplo@example.com",
"name": "Lucía",
"last_name": "Ejemplo",
"sex": {
"id": 2,
"names": { "es": "Femenino", "en": "Female", "pt": "Feminino" }
},
"birthdate": "2004-05-14"
}
],
"message": null,
"errors": null,
"meta": {
"pagination": null,
"cursor": { "next_cursor": "eyJpZCI6NDJ9", "has_more": true }
}
}GET /api/v1/partner/athletes/{athlete_id} returns the same fields plus current_sheet: the most recent athlete profile, or null if the athlete has none. GET /api/v1/partner/athletes/{athlete_id}/sheets returns all their profiles, from oldest to newest and unpaginated, to see how weight or position changed over time.
Each profile carries id, sheet_date, height (cm), weight (kg) and, as catalog items, sport, position, athlete_type, physical_condition, dominant_arm and dominant_leg. A catalog item is an id with its names in es, en and pt, or null when there is no data. A first profile may come with sheet_date set to 2025-01-01: that is the fixed date assigned on sign-up, not an error.
Only the athletes of the institution given in institution_id are visible: an athlete_id from another institution, a deleted one or a nonexistent one all answer the same, 404 athlete_not_found.
Matching with the partner's athletes
Athletes are matched with those in the partner's system by email. The list does not filter by email: that is what the search is for, which takes the email in the body and returns a page shaped like the list.
curl -X POST "https://app.analytics-api.ivolution.com.ar/api/v1/partner/athletes/search?institution_id=42" \
-H "Authorization: Bearer $IVOLUTION_PARTNER_KEY" \
-H "Content-Type: application/json" \
-d '{"email": "lucia.ejemplo@example.com"}'Matching ignores case and accents, and leading and trailing spaces in the searched email are trimmed. With no matches, the response is 200 with an empty page, not an error; an empty email answers 422.
The email always goes in the body, never in the URL. That way it does not end up in a browser history, an access log or a shared URL.
Notes on matching
- One email can return more than one athlete. Email is not unique within an institution: several athletes may share it. In that case, break the tie with
sexandbirthdatebefore keeping theid. - A 404 on an `id` you already saved (
athlete_not_found) means that athlete is no longer available in the institution: redo the email match instead of retrying the sameid. - In catalogs, the key is the `id`. Sport, position and the other catalog items are matched with the athlete's results by their
id, not by the translated name: theidofsportcorresponds tosport_idin the results, and that ofposition, toposition_id.
Results
Results follow the same contract as the platform's export: each row is one attempt at an exercise, with one flat column per field and the same names as the export, such as heightJump, maxForce or exerciseDate, plus exercise_key and exercise_side.
The export's personal data —email, ID document, date of birth, age and country—, the internal identifiers and the exercise's free-text comment are left out. Only Ivolution Trainer data is returned, whatever source the institution has selected: it is not a parameter that can be requested.
curl "https://app.analytics-api.ivolution.com.ar/api/v1/partner/athletes/4821/results?institution_id=42&from=2026-09-01&to=2026-09-30" \
-H "Authorization: Bearer $IVOLUTION_PARTNER_KEY"{
"status": "success",
"data": [
{
"institution": "Club Deportivo Ejemplo",
"nameAthlete": "Lucía Ejemplo",
"test": "CMJ",
"exercise_key": "cmj",
"exercise_side": null,
"exerciseTypeName": "Countermovement Jump",
"exerciseDate": "2026-09-03T14:20:00Z",
"repetitionNumber": 1,
"heightJump": 32.5,
"maxForce": 1450.3
}
],
"message": null,
"errors": null,
"meta": {
"pagination": null,
"cursor": { "next_cursor": "eyJpZCI6NDJ9", "has_more": true }
}
}Optional filters: evaluation_type, the protocol (for example balance_jumps_with_cea; an unknown one answers 422); test, the exact exercise_key of the exercise; and from and to, inclusive dates by the Argentine calendar day. An inactive athlete returns an empty list.
Columns vary by protocol. Any field may come as null: a row only fills in the columns its protocol produces. Read them by name and tolerate null, without assuming a fixed set.
Usage notes
- Only Ivolution Trainer data comes out. An institution that measures only with Valkyria Trainer shows its athletes and their profiles, but has no results, and its dashboards show no data.
- Field names are the export's, with two equivalences to the athlete resource:
idAthleteis the athlete'sid, andgenderis theirsex, as text; theidof that value comes ingender_id. - Use stable keys to filter and group: the protocol (
evaluation_type) andexercise_key+exercise_side. Spanish labels such astestorTipo_Evaluacionmay change, and the exercise name includes the side. - `from` and `to` cut by the Argentine day (
America/Argentina/Buenos_Aires), andexerciseDatecomes in UTC, withZ. An evaluation taken on June 18 at 21:22 in Argentina comes as2026-06-19T00:22:00Zand is included withto=2026-06-18. - A deleted evaluation stops coming, without notice. This version has no tombstones and no list of deleted items: for it to stop showing, refresh what you already stored by reading that date range again.
- Each query takes several seconds. Filtering by
evaluation_typemakes it considerably faster, because without that filter every protocol is queried. Store what you already read instead of requesting it every time a profile is opened.
Pagination
The athlete list and search, the results and the institution list are paginated by cursor. Request the next page by passing meta.cursor.next_cursor in the cursor parameter, with the same filters; when has_more comes as false, the collection is over.
limit defaults to 10 and is at most 100: a larger value is capped. Athlete profiles and the dashboard catalog are not paginated.
# The cursor is opaque: do not build it by hand or reuse it with other filters.
# Keep the value exactly as it arrived and send it back unchanged.
curl "https://app.analytics-api.ivolution.com.ar/api/v1/partner/athletes?institution_id=42&cursor=eyJpZCI6NDJ9&limit=100" \
-H "Authorization: Bearer $IVOLUTION_PARTNER_KEY"A cursor that is invalid, expired or used with other filters answers 400 invalid_cursor. In that case, request the first page again.
Dashboard catalog
Besides the raw data, Analytics dashboards can be displayed inside the partner's product. The catalog returns, complete and unpaginated, the athlete-scoped dashboards ready to embed.
curl "https://app.analytics-api.ivolution.com.ar/api/v1/partner/dashboards?institution_id=42" \
-H "Authorization: Bearer $IVOLUTION_PARTNER_KEY"{
"status": "success",
"data": [
{
"id": 7,
"names": { "es": "Resumen físico", "en": "Physical summary", "pt": "Resumo físico" },
"analysis_type": null,
"equipment": "platform"
}
],
"message": null,
"errors": null,
"meta": null
}names carries the name in all three languages, and analysis_type is a catalog item or null.
Dashboard `id` values belong to each environment. Take them from the catalog in the test environment and in production; do not hardcode them in your code.
Usage notes
- There are only per-athlete dashboards, with the same Ivolution Trainer data as the results: the signed URL locks the source to Ivolution Trainer, just like the institution and the athlete.
- Equipment comes as a stable key, not as a translated name:
platform,dynamometer,encoder,inertialorphoto_cell, ornullif the dashboard has no associated equipment. - If the dashboard service does not respond, the response is 503
dashboards_unavailable, always withRetry-After: wait that long and retry. The catalog never comes partial, and an empty one must not be stored in its place.
The iframe flow
A dashboard is not embedded with a fixed URL. You request a signed URL for a specific athlete, place it in an iframe, and that URL expires after ten minutes.
Requesting the URL
With the dashboard id and the athlete id. The server checks that the athlete belongs to the institution (otherwise 404 athlete_not_found), that they are active (otherwise 409 athlete_inactive) and that the dashboard is in the catalog (otherwise 404 dashboard_not_available).
Rendering in the iframe
Use the URL as is. Institution, athlete and source are locked in the signature: no dashboard filter can be changed from the client.
Renewing on expiry
There is no auto-refresh. If the view stays open for more than ten minutes, request another URL and replace the src.
curl -X POST "https://app.analytics-api.ivolution.com.ar/api/v1/partner/athletes/4821/dashboards/7/embed-url?institution_id=42" \
-H "Authorization: Bearer $IVOLUTION_PARTNER_KEY" \
-H "Content-Type: application/json" \
-d '{"locale": "en"}'{
"status": "success",
"data": {
"url": "<signed URL: place it as is in the iframe src>",
"expires_at": "2026-09-03T14:30:00Z"
},
"message": null,
"errors": null,
"meta": null
}The body is optional: locale accepts es, en, pt or a regional variant such as pt-BR, and any other value falls back to es.
The dashboard does not allow downloads. The URL is signed with downloads disabled, and the response comes with Cache-Control: no-store: request each URL at the moment you display it.
From linking to the dashboard
The whole journey, in the order the partner's system goes through it: redeem the code, find the athlete, read their results and show their dashboard. It uses the same sample data as the rest of the guide.
# 1. Redeem the link code, only once.
# Keep data.institution_id (42): it is the institution_id of everything that follows.
curl -X POST "https://app.analytics-api.ivolution.com.ar/api/v1/partner/link" \
-H "Authorization: Bearer $IVOLUTION_PARTNER_KEY" \
-H "Content-Type: application/json" \
-d '{"code": "EjemploDeCodigoDeVinculacion-NoEsReal_00000"}'
# 2. Find the athlete by email. If more than one comes back, break the tie with sex and birthdate.
# Keep their id (4821).
curl -X POST "https://app.analytics-api.ivolution.com.ar/api/v1/partner/athletes/search?institution_id=42" \
-H "Authorization: Bearer $IVOLUTION_PARTNER_KEY" \
-H "Content-Type: application/json" \
-d '{"email": "lucia.ejemplo@example.com"}'
# 3. Read their results for one protocol over one month, page by page with meta.cursor.
curl "https://app.analytics-api.ivolution.com.ar/api/v1/partner/athletes/4821/results?institution_id=42&evaluation_type=balance_jumps_with_cea&from=2026-09-01&to=2026-09-30" \
-H "Authorization: Bearer $IVOLUTION_PARTNER_KEY"
# 4. Pick a dashboard from this environment's catalog: its id (7) is not hardcoded.
curl "https://app.analytics-api.ivolution.com.ar/api/v1/partner/dashboards?institution_id=42" \
-H "Authorization: Bearer $IVOLUTION_PARTNER_KEY"
# 5. When the view opens, request the signed URL of that athlete's dashboard.
curl -X POST "https://app.analytics-api.ivolution.com.ar/api/v1/partner/athletes/4821/dashboards/7/embed-url?institution_id=42" \
-H "Authorization: Bearer $IVOLUTION_PARTNER_KEY" \
-H "Content-Type: application/json" \
-d '{"locale": "en"}'<!-- src: the data.url returned by step 5, as is. It expires after ten minutes. -->
<iframe
src="SIGNED_URL_FROM_STEP_5"
title="Dashboard of Lucía Ejemplo"
width="100%"
height="600"
></iframe>What you store is the institution_id, the athlete's id and the results already read. The dashboard URL is not stored: request it every time the view opens, and again if it is still open after ten minutes.
Errors and limits
Every error has the same shape: status set to error, a readable message and the errors list, whose code is what the partner's system should read. Several of these codes are not technical failures: they are business states the partner's system must be able to communicate.
{
"status": "error",
"data": null,
"message": "Institution has no active trial or subscription.",
"errors": [
{
"code": "subscription_required",
"message": "Institution has no active trial or subscription.",
"field": null
}
],
"meta": null
}| Code | HTTP | What happened | What to do |
|---|---|---|---|
invalid_cursor | 400 | The cursor is invalid, expired or was used with other filters. | Request the first page again. |
invalid_credential | 401 | The credential is missing or does not work. | Check the credential in use and its environment. |
insufficient_scope | 403 | The credential lacks the scope the route requires. | Ask Ivolution about the scope. |
integration_revoked | 403 | The institution ended the integration on purpose. | Do not retry: a new authorization is needed. |
subscription_required | 403 | The integration exists, but the institution has no current subscription or trial. | Notify the institution. It is resolved on their side. |
athlete_not_found | 404 | The athlete does not exist, was deleted or belongs to another institution. | Redo the email match; do not retry the same athlete_id. |
dashboard_not_available | 404 | The dashboard is not in the catalog. | Read that environment's catalog again. |
integration_not_found | 404 | There is no integration with that institution. | Request a new authorization. |
athlete_inactive | 409 | The athlete is inactive: their dashboard is not signed. | Do not show that athlete's dashboard. |
integration_expired | 409 | The link code expired without being redeemed. | The institution generates a new code. |
integration_pending | 409 | The institution authorized, but the code has not been redeemed yet. | Redeem the code. |
link_code_invalid | 409 | The code does not exist, belongs to another partner, was already redeemed or expired. | Ask the institution for a new code. |
validation_error | 422 | The request did not pass validation. | Fix the request. |
rate_limited | 429 | The call limit was exceeded. | Wait for the time given in Retry-After and retry. |
dashboards_unavailable | 503 | The catalog or the dashboard cannot be confirmed right now. | Retry after Retry-After. |
results_unavailable | 503 | Results are unavailable for the moment. | Retry later, after Retry-After if it is present. |
Usage limits
Limits are counted per credential, in three independent quotas. When one is exceeded, the response is 429 rate_limited with Retry-After.
| Quota | Limit |
|---|---|
| Reads: every route except the next two | 100 per minute |
Link code redemption (/link) | 10 per minute |
Signed dashboard URL (/embed-url) | 30 per minute |
Test environment
The test environment has a demo institution with synthetic athletes and profiles. No real person's data flows through that environment, which makes it possible to develop without confidentiality agreements in the way.
There is no self-service sign-up: the test credential, the demo institution's institution_id and the environment's address are provided by Ivolution when the partner is onboarded. The integration with the demo institution arrives already active, and from there on the routes, responses and errors are the same as in production.
Each credential works in a single environment. The test one does not work in production, nor the production one in test: the credential's prefix says which one it belongs to.
Responsibilities, attribution and purge
The data belongs to the institution that shared it, not to the partner or to Ivolution. The integration involves taking on three obligations:
- Custody. The data read becomes the partner's responsibility, to be handled with the same care as its own.
- Attribution. Wherever performance data is displayed, please state that it comes from Ivolution.
- Purge on request. If an institution revokes the integration and requests the deletion of what was read, the partner must be able to do it.
Email correlation. The same person may be an athlete of two institutions that authorized the partner: they appear in each one's list, with a different id, and their results are read separately, each with its own institution's institution_id. Recognizing them by email is an accepted correlation. The API, on the other hand, does not cross data: each read returns only what the given institution produced.
Revocation deletes nothing on the partner's side. When an institution revokes, access is cut off on the next call, but the data already obtained remains in the partner's system: deleting it on request is up to the partner.