Skip to main content

API reference

The octo.taxi API

Read your Cornerstone data through octo.taxi, alongside Cornerstone: the same records, kept in sync near-realtime, and your API calls never reach your portal. Three ways in: Cornerstone's own format, a simpler camelCase API, and an OData feed for Power BI.

Base URL

https://api.octo.taxi
/v2: simplified
camelCase fields, readable paths, the sync time in every list answer.
/v1: Cornerstone mirror
Cornerstone's records with Cornerstone's field names and paging.
/v2/odata: BI feed
OData v4 for Power BI and other BI tools, same fields as /v2.

Quickstart

From nothing to every employee in three steps.

1. Create a key

In octo.taxi, open System, API keys (tenant owners) and choose Create API key. Keep "Read-only (all objects except restricted ones)" or pick the objects the key may read. The key starts with octx_sk_ and is shown once: store it in your password manager or secret store.

Shell
export OCTOTAXI_API_KEY="octx_sk_..."

2. Make the first call

Send the key as a bearer token. The answer is the first page of employees and a meta block with the total, the cursor of the next page and syncedAt, the time of the last completed sync of that object.

Ask for full pages (limit=200) and follow the cursor to the end: with the minimum group size switched on, a list is refused only when its whole result holds fewer than five people, never because a page is short (see Privacy).

curl "https://api.octo.taxi/v2/employees?limit=200" \
  -H "Authorization: Bearer $OCTOTAXI_API_KEY"
Response 200
{
  "data": [
    {
      "id": "100042",
      "firstName": "Ada",
      "lastName": "Example",
      "email": "ada.example@example.com",
      "status": "1",
      "hireDate": "2021-03-15T00:00:00.000Z",
      "terminationDate": null,
      "managerId": "100003",
      "jobTitle": "Branch Manager",
      "division": "Sales",
      "department": "Branch Operations",
      "location": "Berlin"
    }
  ],
  "meta": {
    "total": 270,
    "pageSize": 200,
    "hasMore": true,
    "cursor": "eyJpZCI6IjEwMDIwMCIsInQiOjI3MH0",
    "syncedAt": "2026-10-07T08:30:12.000Z"
  }
}

3. Read every page

Pass meta.cursor back as cursor until meta.hasMore is false. Over the rate limit the API answers 429 with Retry-After: wait that many seconds and repeat the same request.

# Page 1, then repeat with the cursor from meta.cursor until meta.hasMore is false
curl "https://api.octo.taxi/v2/employees?limit=200" \
  -H "Authorization: Bearer $OCTOTAXI_API_KEY"

curl "https://api.octo.taxi/v2/employees?limit=200&cursor=<meta.cursor>" \
  -H "Authorization: Bearer $OCTOTAXI_API_KEY"

Authentication

Every request carries an API key. The key decides the tenant, the objects it may read and the entities it is limited to.

Authorizationheader
Bearer octx_sk_... (checked first)
X-API-Keyheader
octx_sk_... (what the Power BI template uses)

Scopes. Each endpoint needs the scope of its Cornerstone object, for example obj_users_core:read for employees; the reference names it per endpoint. A key without it gets 403 INSUFFICIENT_SCOPE, and required_scope names the missing scope. "Read-only (all objects except restricted ones)" grants every open object scope; restricted data (pay, performance, evaluations, recruiting, terminations and leave, logins per person, individual survey answers) and delegate are granted by name only, by the tenant owner with a confirmed legal basis.

Expiry. Every key expires: 90 days by default, up to 365, chosen when it is created and renewable under System, API keys. An expired key answers 401 with "code": "API_KEY_EXPIRED" and the date; the key list flags a key 14 days before it expires.

Entities. A key can be limited to entities (for example one legal entity). If your tenant scopes data by entity, a key without an entity reads nothing.

Rotation. Create the new key, switch your client, then revoke the old one. A revoked key answers 401 at once.

Keys belong on a server or in a BI service, never in a browser page: anyone who sees a key can read what it may read.

curl "https://api.octo.taxi/v1/me" \
  -H "Authorization: Bearer $OCTOTAXI_API_KEY"
Response 200
{ "mode": "apikey", "tenant": "<your tenant>" }
Response 401
{ "error": "Unauthorized: invalid API key" }
Response 401
{ "error": "Unauthorized: API key expired", "code": "API_KEY_EXPIRED", "expired_at": "2027-01-07T12:00:00.000Z" }

The three APIs

All three read the same synced tables under https://api.octo.taxi. They differ in names, paging and privacy.

/v2recommended
Readable paths (/v2/employees), camelCase fields, { data, meta } answers with the sync time, cursor paging with up to 200 rows a page. Masking and the minimum group size apply.
/v1
Cornerstone's records unchanged: Cornerstone's field names, the Data Exporter's { value, @odata.nextLink } envelope and its paging (1,000 rows by default, up to 10,000). Also under Cornerstone's own path, so an existing integration only changes host and token. No masking, no minimum group size (see Privacy).
/v2/odata
OData v4 over ten /v2 endpoints for BI tools: $select, $filter, $orderby, $top, $count, @odata.nextLink. Same fields and privacy as /v2.

The web address octo.taxi does not serve the API: always call https://api.octo.taxi. Every endpoint only reads and answers JSON: all are GET except POST /v1/query, which takes its query as a JSON body (see Cornerstone mirror). Cross-origin requests are allowed, but keep keys out of browsers.

Pagination

Every list is paged. Follow the cursor or link the API gives you; never compute offsets yourself.

/v2
limit = rows per page (50 by default, at most 200). The answer's meta.cursor goes back as cursor for the next page; meta.hasMore says whether there is one; meta.total counts all matching rows (counted on the first page; later pages carry that number in the cursor).
/v1
$top (or limit, or the header Prefer: odata.maxpagesize=N) = rows per page, 1,000 by default, at most 10,000. Follow @odata.nextLink until it is absent. $skip is refused (400): use the link.
/v2/odata
$top = rows per page (50 by default, at most 200). Follow @odata.nextLink (it keeps your $select, $filter, $orderby and $top). $count=true adds @odata.count. $skip other than 0 answers 501.

Filtering, sorting, fields

On /v2 a filter is filter[field][operator]=value with the endpoint's field names; several filters are combined with and. sort=field sorts ascending, sort=-field descending; fields=a,b returns only those fields.

Unknown fields and operators are ignored, not refused. Check a filter by comparing meta.total with and without it. A value that does not fit the field's type (text on a date or number field) is refused with 400 INVALID_FILTER, and a filter or sort on a field of a restricted category the key may not read with 403 SENSITIVE_DATA_RESTRICTED.

On /v1 the same filter[...] syntax and $filter work on Cornerstone's field names and $select picks fields. $orderby=field asc|desc (or sort=field / sort=-field) sorts by one field's text value, empty values last, ties by the record key; paging stays stable under a sort because the link carries the sort value and the key. Without a sort, rows come in the table's key order. As on /v2, a sort on an unknown field is ignored, not refused (the key order stays). Text is compared in the database's collation, so upper and lower case do not decide the order (a, B, c, not A to Z before a to z).

eq
equals; a comma-separated value matches any of them. filter[status][eq]=1,2
neq
does not equal. filter[status][neq]=2
gt
greater than. filter[hireDate][gt]=2024-01-01
gte
greater than or equal. filter[score][gte]=80
lt
less than. filter[dueDate][lt]=2026-12-31
lte
less than or equal. filter[score][lte]=100
contains
contains, case-insensitive. filter[jobTitle][contains]=manager
notContains
does not contain (empty values match). filter[jobTitle][notContains]=intern
startsWith
starts with, case-insensitive. filter[email][startsWith]=anna
endsWith
ends with, case-insensitive. filter[email][endsWith]=@example.com
matches
regular expression, case-insensitive, up to 200 characters. filter[jobTitle][matches]=^senior
isEmpty
empty or null (the value is ignored). filter[exemptDt][isEmpty]=true
isNotEmpty
has a value (the value is ignored). filter[terminationDate][isNotEmpty]=true
curl "https://api.octo.taxi/v2/learning?filter[isRequired][eq]=true&filter[exemptDt][isEmpty]=true&sort=-dueDate&limit=200" \
  -H "Authorization: Bearer $OCTOTAXI_API_KEY"

OData $filter

field eq 'value'
also ne, gt, ge, lt, le; numbers, true and false without quotes
field eq null
is empty (ne null = has a value)
field in ('a','b')
any of the values
contains(field,'text')
also startswith and endswith; no spaces inside the brackets
a eq 1 and b eq 2
only and; or, not and brackets answer 400

Responses and errors

Success is 200 with JSON. An error has a status, a code and a message.

400 BAD_REQUEST
An invalid asOf date, a $filter the API cannot read, $skip on /v1, a bad JSON body
400 INVALID_FILTER
A filter value that does not fit the field's type (text on a date or number field); column names the field
400 TEMPORAL_NOT_SUPPORTED
asOf on an endpoint without history
401
Missing or invalid API key; the body is { "error": "Unauthorized: ..." } (a string, not an object)
401 API_KEY_EXPIRED
The key's expiry date has passed; error stays a string, code and expired_at ride next to it. Renew the key under System, API keys
403 INSUFFICIENT_SCOPE
The key lacks the endpoint's scope; required_scope names it
403 SENSITIVE_DATA_RESTRICTED
A filter or sort on a field of a restricted category the key may not read; field names it
403 PRIVACY_BLOCKED
The tenant's privacy rules refuse the answer, e.g. message cohort_too_small or k_anonymity_unprovable (see Privacy)
404 NOT_FOUND
Unknown record, entity set, custom endpoint, /v1 resource or path
404 NO_COVERAGE_FOR_DATE
/v2/employees/{id}?asOf= with no version of the record on that date
429 RATE_LIMITED
Over the key's limit (600 a minute by default) or your organisation's budget; wait Retry-After seconds
500 INTERNAL_ERROR
An unexpected error on our side
501 NOT_IMPLEMENTED
$skip greater than 0 on /v2/odata (use @odata.nextLink)
503 NOT_AVAILABLE
One of the four /v2 endpoints listed as unavailable below
503 QUERY_TIMEOUT
The query ran longer than 10 seconds and was stopped; narrow the filter or retry after Retry-After seconds

Dates and times are ISO 8601 in UTC (2026-10-07T08:30:12.000Z); ids are strings on /v2. Codes such as status are Cornerstone's ids, not labels.

Response 403
{
  "error": {
    "code": "INSUFFICIENT_SCOPE",
    "message": "API key is missing required scope \"obj_users_core:read\".",
    "required_scope": "obj_users_core:read"
  }
}

Rate limits

Your integrations read at octo.taxi's limits, not Cornerstone's. The defaults are set for security, not capacity, and raising them is no problem.

600

requests a minute per API key by default

120,000

rows a minute per key on /v2, 200 a page

0

calls to your Cornerstone portal per API request

120

Data Exporter calls a minute Cornerstone allows per environment, for comparison

Not bound by Cornerstone's throttling. Every request with an API key is answered from octo.taxi's own copy of your data and never calls your Cornerstone portal, so it does not count against your portal's Cornerstone API limits. Cornerstone documents 120 Data Exporter calls a minute in total per environment, and its other APIs throttle per endpoint. octo.taxi's own sync stays within the share of that budget your admin sets, so your other Cornerstone integrations keep the rest.

Scaling is no problem. The defaults protect your data: a leaked or misconfigured key cannot pull everything at once. When your integration needs more, tell us and we raise the limit of your key and the budget of your organisation; the key's entry under System, API keys shows the limit that applies. Extra-high rate limits are available as an add-on.

How the limits count. A key's limit counts every client that uses the key. Your organisation also has a burst budget across all its keys and embedded widgets that grows with its size, so one organisation's peak never slows another's. Requests with a valid key are not limited by the address they come from, apart from a protective ceiling far above any key's limit. Repeated failed attempts from one address (a missing, unknown, revoked or expired key) block that address for a short while, valid keys included.

Over a limit the API answers 429 RATE_LIMITED with Retry-After: the seconds until a request is allowed again. Wait that long and repeat the same request. There are no other rate-limit headers.

Response 429
HTTP/1.1 429 Too Many Requests
Retry-After: 12

{ "error": { "code": "RATE_LIMITED", "message": "Too many requests. Please try again later." } }

Data freshness

octo.taxi keeps its copy of your Cornerstone data current near-realtime and picks up changes every few minutes. Our goal is to add at most 15 minutes to the delay Cornerstone's own reporting data already has; Cornerstone's delay is not in our hands.

meta.syncedAt on /v2 is the time of the last complete sync of that object (null before the first). On /v1 every record keeps Cornerstone's own change stamp _last_touched_dt_utc when Cornerstone sends it.

Privacy

Your tenant's privacy settings apply to every key. /v1 is the one deliberate exception for masking and group size.

Minimum group size/v2, /v2/odata, custom
On by default (k = 5). A list is refused with 403 PRIVACY_BLOCKED, message cohort_too_small, when its whole result - your filters, the key's entities - holds fewer than k different people; it is judged once per request on the whole result, so every page of an allowed list arrives, a short last page included. A single record (/v2/employees/{id}) is refused while the minimum group size is on. Rows without a person (org units, trainings, certification definitions) have no minimum group. The refusal never says how many people matched. A result whose people cannot be counted is refused as well, never served as "aggregated": message k_anonymity_unprovable (aggregations such as a count per department are not a shape of this API).
Masking/v2, /v2/odata, custom
Fields your admins mask are left out of the answer.
Entities and auditevery API
Entity limits of the key and of the tenant apply everywhere; with audit logging on, every read is recorded in the tenant's audit log.
/v1 mirrorexception
/v1 returns Cornerstone's records 1:1, so masking and the minimum group size do not apply there. If your tenant relies on them, give integrations /v2 keys only.

Endpoints (/v2)

Every endpoint is GET, answers { data, meta } and takes the list parameters below. A field marked not filled exists but no synced Cornerstone object fills it yet: it is always null.

Core HR

Employees

GET/v2/employees

GET/v2/employees/{id}

People of the portal with their manager and the names of their position, division, department and location OUs.

Scope
obj_users_core:read
Cornerstone
users_core (Data Exporter)
OData
/v2/odata/Employees

Query parameters

limitintegeroptional
Rows per page, 50 by default, at most 200.
cursorstringoptional
The previous answer's meta.cursor.
sortstringoptional
A field name, - in front for descending. Default id.
fieldsstringoptional
Comma-separated field names to return.
filter[field][op]stringoptional
See Filtering.
asOfdateoptional
YYYY-MM-DD: the rows as they were at the end of that day (UTC). See Point in time.

Fields

idstring
Unique identifier of the row (Cornerstone user_id)
firstNamestring
First name (Cornerstone user_name_first)
lastNamestring
Last name (Cornerstone user_name_last)
emailstring
E-mail address (Cornerstone user_email)
statusstring
Status code as Cornerstone sends it (Cornerstone user_status_id)
hireDatedate-time
Original hire date (Cornerstone user_hire_dt_orig)
terminationDatedate-time
Termination date, empty while employed (Cornerstone user_termination_dt)
managerIdstring
The person's manager (user id) (Cornerstone user_mgr_id)
jobTitlestring
Name of the person's position OU
divisionstring
Name of the person's division OU
departmentstring
Name of the person's department OU
locationstring
Name of the person's location OU
curl "https://api.octo.taxi/v2/employees?limit=200" \
  -H "Authorization: Bearer $OCTOTAXI_API_KEY"

Org units

GET/v2/org-units

Organisational units (OUs) of every type with their parent.

Scope
obj_ou_core:read
Cornerstone
ou_core (Data Exporter)
OData
/v2/odata/OrgUnits

Query parameters

limitintegeroptional
Rows per page, 50 by default, at most 200.
cursorstringoptional
The previous answer's meta.cursor.
sortstringoptional
A field name, - in front for descending. Default name.
fieldsstringoptional
Comma-separated field names to return.
filter[field][op]stringoptional
See Filtering.

Fields

idstring
Unique identifier of the row (Cornerstone ou_id)
namestring
Name (Cornerstone title)
typestring
OU type id (Cornerstone type_id)
parentIdstring
Parent OU (Cornerstone parent_id)
curl "https://api.octo.taxi/v2/org-units?limit=200" \
  -H "Authorization: Bearer $OCTOTAXI_API_KEY"

OU hierarchy

GET/v2/org-hierarchy

Ancestor and descendant pairs of the OU tree with their distance.

Scope
obj_ou_hierarchy_core:read
Cornerstone
ou_hierarchy_core (Data Exporter)
OData
/v2/odata/OrgHierarchy

Query parameters

limitintegeroptional
Rows per page, 50 by default, at most 200.
cursorstringoptional
The previous answer's meta.cursor.
sortstringoptional
A field name, - in front for descending. Default level.
fieldsstringoptional
Comma-separated field names to return.
filter[field][op]stringoptional
See Filtering.

Fields

ouIdstring
The descendant OU (Cornerstone descendant_ou_id)
parentIdstring
The ancestor OU (Cornerstone ancestor_ou_id)
pathstring
Relative path between the two (Cornerstone relative_path)
levelinteger
Distance between the two (1 = direct parent) (Cornerstone distance)
curl "https://api.octo.taxi/v2/org-hierarchy?limit=200" \
  -H "Authorization: Bearer $OCTOTAXI_API_KEY"

Employee assignments

GET/v2/employee-assignments

Which OU a person belongs to, per OU type: the bridge between people and OUs.

Scope
obj_user_ou_core:read
Cornerstone
user_ou_core (Data Exporter)
OData
/v2/odata/EmployeeAssignments

Query parameters

limitintegeroptional
Rows per page, 50 by default, at most 200.
cursorstringoptional
The previous answer's meta.cursor.
sortstringoptional
A field name, - in front for descending. Default userId.
fieldsstringoptional
Comma-separated field names to return.
filter[field][op]stringoptional
See Filtering.

Fields

userIdstring
Person (Cornerstone user id) (Cornerstone user_id)
ouIdstring
Org unit (Cornerstone ou_id)
ouTypestring
OU type id (Cornerstone ou_type_id)
curl "https://api.octo.taxi/v2/employee-assignments?limit=200" \
  -H "Authorization: Bearer $OCTOTAXI_API_KEY"

Custom field values

GET/v2/custom-fields

Values of user-defined fields per person, one row per person and field.

Scope
obj_users_cf_core:read
Cornerstone
users_cf_core (Data Exporter)
OData
/v2/odata/CustomFields

Query parameters

limitintegeroptional
Rows per page, 50 by default, at most 200.
cursorstringoptional
The previous answer's meta.cursor.
sortstringoptional
A field name, - in front for descending. Default userId.
fieldsstringoptional
Comma-separated field names to return.
filter[field][op]stringoptional
See Filtering.

Fields

userIdstring
Person (Cornerstone user id) (Cornerstone user_id)
customFieldIdstring
Custom field id (Cornerstone custom_field_id)
valuestring
Value (Cornerstone custom_field_value)
labelstringnot filled
Field label
curl "https://api.octo.taxi/v2/custom-fields?limit=200" \
  -H "Authorization: Bearer $OCTOTAXI_API_KEY"

User statuses

GET/v2/status-changes

Cornerstone's list of user statuses (user_status_core). Despite the path it holds no status history.

Scope
obj_user_status_core:read
Cornerstone
user_status_core (Data Exporter)
OData
/v2/odata/StatusChanges

Query parameters

limitintegeroptional
Rows per page, 50 by default, at most 200.
cursorstringoptional
The previous answer's meta.cursor.
sortstringoptional
A field name, - in front for descending. Default -effectiveDate.
fieldsstringoptional
Comma-separated field names to return.
filter[field][op]stringoptional
See Filtering.

Fields

userIdstring
Status id (historical name of the field) (Cornerstone status_id)
statusIdstringnot filled
Not filled
effectiveDatedate-timenot filled
Not filled
curl "https://api.octo.taxi/v2/status-changes?limit=200" \
  -H "Authorization: Bearer $OCTOTAXI_API_KEY"

Relationships

GET/v2/relationships

Dynamic relations between people (Cornerstone user_dynamic_relation_core).

Scope
obj_user_dynamic_relation_core:read
Cornerstone
user_dynamic_relation_core (Data Exporter)
OData
/v2/odata/Relationships

Query parameters

limitintegeroptional
Rows per page, 50 by default, at most 200.
cursorstringoptional
The previous answer's meta.cursor.
sortstringoptional
A field name, - in front for descending. Default id.
fieldsstringoptional
Comma-separated field names to return.
filter[field][op]stringoptional
See Filtering.

Fields

idstring
Unique identifier of the row
userIdstring
Person (Cornerstone user id) (Cornerstone user_dynamic_relation_user_id)
relatedUserIdstringnot filled
The related person
relationTypestringnot filled
Relation type
curl "https://api.octo.taxi/v2/relationships?limit=200" \
  -H "Authorization: Bearer $OCTOTAXI_API_KEY"

Learning

Learning (transcripts)

GET/v2/learning

One row per person and training on the transcript: status, due date, completion, score, the required flag and the exemption date.

Scope
obj_transcript_core:read
Cornerstone
transcript_core (Data Exporter)
OData
/v2/odata/Learning

Query parameters

limitintegeroptional
Rows per page, 50 by default, at most 200.
cursorstringoptional
The previous answer's meta.cursor.
sortstringoptional
A field name, - in front for descending. Default -completionDate.
fieldsstringoptional
Comma-separated field names to return.
filter[field][op]stringoptional
See Filtering.

Fields

idstring
Unique identifier of the row
userIdstring
Person (Cornerstone user id) (Cornerstone transc_user_id)
objectIdstring
Training (learning object id) (Cornerstone transc_object_id)
statusstring
Status code as Cornerstone sends it (Cornerstone user_lo_status_id)
completionDatedate-time
Completion date (Cornerstone user_lo_comp_dt)
dueDatedate-time
Due date (Cornerstone user_lo_min_due_date)
scorenumber
Score (Cornerstone user_lo_score)
isRequiredboolean
Required for the person (Cornerstone is_required). Mandatory = isRequired and no exemptDt (Cornerstone is_required)
exemptDtdate-time
Date of the exemption, empty when not exempted (Cornerstone exempt_dt) (Cornerstone exempt_dt)
curl "https://api.octo.taxi/v2/learning?limit=200" \
  -H "Authorization: Bearer $OCTOTAXI_API_KEY"

Trainings

GET/v2/learning-objects

The training catalogue: title, type and provider.

Scope
obj_training_core:read
Cornerstone
training_core (Data Exporter)
OData
/v2/odata/LearningObjects

Query parameters

limitintegeroptional
Rows per page, 50 by default, at most 200.
cursorstringoptional
The previous answer's meta.cursor.
sortstringoptional
A field name, - in front for descending. Default title.
fieldsstringoptional
Comma-separated field names to return.
filter[field][op]stringoptional
See Filtering.

Fields

idstring
Unique identifier of the row (Cornerstone object_id)
titlestring
Title
typestring
Learning object type (Cornerstone lo_object_type) (Cornerstone lo_object_type)
providerstring
Provider id (Cornerstone lo_provider_id)
durationMinutesintegernot filled
Duration in minutes
curl "https://api.octo.taxi/v2/learning-objects?limit=200" \
  -H "Authorization: Bearer $OCTOTAXI_API_KEY"

Certifications

GET/v2/certifications

Certification holders: one row per person and certification with the status and the dates of the person's current period, from Cornerstone's Reporting API view vw_rpt_certification2 (pulled once a day). Field names are the view's own.

Scope
vw_rpt_certification2:read
Cornerstone
vw_rpt_certification2 (Reporting API view)
OData
/v2/odata/Certifications

Query parameters

limitintegeroptional
Rows per page, 50 by default, at most 200.
cursorstringoptional
The previous answer's meta.cursor.
sortstringoptional
A field name, - in front for descending. Default -periodExpirationDate.
fieldsstringoptional
Comma-separated field names to return.
filter[field][op]stringoptional
See Filtering.

Fields

idstring
octo.taxi's row id: <userId>:<certId>
userIdstring
Person (Cornerstone user id) (Cornerstone user_id)
certIdstring
Certification (see /v2/certification-definitions) (Cornerstone cert_id)
titlestring
Certification title (certification_local_core, else the view vw_rpt_cert_local) (Cornerstone title)
userCertStatusstring
User certification status as Cornerstone names it (Certified, Expired, In Progress, ...) (Cornerstone user_cert_status)
userCertStatusIdstring
Status id (Cornerstone user_cert_status_id)
userPeriodCompletionDatedate-time
Completion date of the current period (Cornerstone user_period_completion_date)
firstPeriodCompletionDatedate-time
Completion date of the first period (Cornerstone first_period_completion_date)
periodExpirationDatedate-time
Expiry date of the current period (Cornerstone period_expiration_date)
periodDueDatedate-time
Due date of the current period (Cornerstone period_due_date)
certAssignDtdate-time
Assignment date (Cornerstone cert_assign_dt)
statusChangeDtdate-time
Last status change (Cornerstone status_change_dt)
userCertIsArchivedboolean
Archived for this person (Cornerstone user_cert_is_archived)
certIsOnHoldboolean
Certification on hold (Cornerstone cert_is_on_hold)
curl "https://api.octo.taxi/v2/certifications?limit=200" \
  -H "Authorization: Bearer $OCTOTAXI_API_KEY"

Certification definitions

GET/v2/certification-definitions

The certifications themselves (Cornerstone certification_core): one row per certification, no holders - those are /v2/certifications.

Scope
obj_certification_core:read
Cornerstone
certification_core (Data Exporter)
OData
/v2/odata/CertificationDefinitions

Query parameters

limitintegeroptional
Rows per page, 50 by default, at most 200.
cursorstringoptional
The previous answer's meta.cursor.
sortstringoptional
A field name, - in front for descending. Default id.
fieldsstringoptional
Comma-separated field names to return.
filter[field][op]stringoptional
See Filtering.

Fields

idstring
Certification id (Cornerstone cert_id)
isActivestring
Active flag of the certification, as Cornerstone sends it (Cornerstone is_active)
curl "https://api.octo.taxi/v2/certification-definitions?limit=200" \
  -H "Authorization: Bearer $OCTOTAXI_API_KEY"

Training assignments

GET/v2/assignments

Assignments of the Learning Assignment Tool with their training and due date.

Scope
obj_training_assignment_core:read
Cornerstone
training_assignment_core (Data Exporter)

Query parameters

limitintegeroptional
Rows per page, 50 by default, at most 200.
cursorstringoptional
The previous answer's meta.cursor.
sortstringoptional
A field name, - in front for descending. Default -dueDate.
fieldsstringoptional
Comma-separated field names to return.
filter[field][op]stringoptional
See Filtering.

Fields

idstring
Unique identifier of the row (Cornerstone assignment_id)
userIdstringnot filled
Person (Cornerstone user id)
objectIdstring
Training (learning object id) (Cornerstone object_id)
dueDatedate-time
Due date (Cornerstone due_date_fixed_dt)
statusstring
Status code as Cornerstone sends it (Cornerstone status_id)
isTrainingRequiredboolean
The assignment rule marks its training as required (Cornerstone is_training_required). A flag of the rule, not of a person: whether a training is mandatory for someone is isRequired on /v2/learning (Cornerstone is_training_required)
curl "https://api.octo.taxi/v2/assignments?limit=200" \
  -H "Authorization: Bearer $OCTOTAXI_API_KEY"

Sessions

GET/v2/sessions

Scheduled ILT session parts with start and end time.

Scope
obj_training_part2_core:read
Cornerstone
training_part2_core (Data Exporter)

Query parameters

limitintegeroptional
Rows per page, 50 by default, at most 200.
cursorstringoptional
The previous answer's meta.cursor.
sortstringoptional
A field name, - in front for descending. Default -startDate.
fieldsstringoptional
Comma-separated field names to return.
filter[field][op]stringoptional
See Filtering.

Fields

idstring
Unique identifier of the row (Cornerstone schedule_id)
objectIdstring
Training (learning object id) (Cornerstone object_id)
startDatedate-time
Start (Cornerstone start_dt_utc)
endDatedate-time
End (Cornerstone end_dt_utc)
capacityintegernot filled
Capacity
curl "https://api.octo.taxi/v2/sessions?limit=200" \
  -H "Authorization: Bearer $OCTOTAXI_API_KEY"

Providers

GET/v2/providers

Training providers.

Scope
obj_training_provider_core:read
Cornerstone
training_provider_core (Data Exporter)

Query parameters

limitintegeroptional
Rows per page, 50 by default, at most 200.
cursorstringoptional
The previous answer's meta.cursor.
sortstringoptional
A field name, - in front for descending. Default name.
fieldsstringoptional
Comma-separated field names to return.
filter[field][op]stringoptional
See Filtering.

Fields

idstring
Unique identifier of the row (Cornerstone provider_id)
namestring
Name (Cornerstone provider_name)
typestringnot filled
Type
curl "https://api.octo.taxi/v2/providers?limit=200" \
  -H "Authorization: Bearer $OCTOTAXI_API_KEY"

Curriculum structure

GET/v2/curricula

The trainings inside each curriculum, in display order.

Scope
obj_curriculum_structure_core:read
Cornerstone
curriculum_structure_core (Data Exporter)

Query parameters

limitintegeroptional
Rows per page, 50 by default, at most 200.
cursorstringoptional
The previous answer's meta.cursor.
sortstringoptional
A field name, - in front for descending. Default id.
fieldsstringoptional
Comma-separated field names to return.
filter[field][op]stringoptional
See Filtering.

Fields

idstring
Unique identifier of the row
objectIdstring
Training (learning object id) (Cornerstone object_id)
sectioninteger
Display position inside the curriculum (Cornerstone display_seq)
isRequiredboolean
Cornerstone is_auto_register of the curriculum item (Cornerstone is_auto_register)
curl "https://api.octo.taxi/v2/curricula?limit=200" \
  -H "Authorization: Bearer $OCTOTAXI_API_KEY"

Purchases

GET/v2/purchases

Training purchases with amount and currency.

Scope
obj_training_purchase_core:read
Cornerstone
training_purchase_core (Data Exporter)

Query parameters

limitintegeroptional
Rows per page, 50 by default, at most 200.
cursorstringoptional
The previous answer's meta.cursor.
sortstringoptional
A field name, - in front for descending. Default id.
fieldsstringoptional
Comma-separated field names to return.
filter[field][op]stringoptional
See Filtering.

Fields

idstring
Unique identifier of the row (Cornerstone training_purchase_id)
objectIdstring
Training (learning object id) (Cornerstone ref)
userIdstring
Person (Cornerstone user id) (Cornerstone purchaser_id)
amountnumber
Amount (Cornerstone total)
currencystring
Currency id (Cornerstone currency_id)
curl "https://api.octo.taxi/v2/purchases?limit=200" \
  -H "Authorization: Bearer $OCTOTAXI_API_KEY"

Ratings

GET/v2/ratings

Course ratings by learners.

Scope
obj_course_rating_core:read
Cornerstone
course_rating_core (Data Exporter)

Query parameters

limitintegeroptional
Rows per page, 50 by default, at most 200.
cursorstringoptional
The previous answer's meta.cursor.
sortstringoptional
A field name, - in front for descending. Default id.
fieldsstringoptional
Comma-separated field names to return.
filter[field][op]stringoptional
See Filtering.

Fields

idstring
Unique identifier of the row (Cornerstone rating_id)
objectIdstring
Training (learning object id) (Cornerstone object_id)
userIdstring
Person (Cornerstone user id) (Cornerstone user_id)
ratingnumber
Rating (Cornerstone rating)
curl "https://api.octo.taxi/v2/ratings?limit=200" \
  -H "Authorization: Bearer $OCTOTAXI_API_KEY"

Performance

Goals

GET/v2/goals

Performance goals with owner, dates, weight and progress.

Scope
obj_goal_core:read
Cornerstone
goal_core (Data Exporter)

Query parameters

limitintegeroptional
Rows per page, 50 by default, at most 200.
cursorstringoptional
The previous answer's meta.cursor.
sortstringoptional
A field name, - in front for descending. Default -dueDate.
fieldsstringoptional
Comma-separated field names to return.
filter[field][op]stringoptional
See Filtering.

Fields

idstring
Unique identifier of the row (Cornerstone goal_id)
userIdstring
Person (Cornerstone user id)
titlestring
Title (Cornerstone title)
descriptionstring
Description (Cornerstone descr)
statusstring
Status code as Cornerstone sends it (Cornerstone status_id)
goalTypestring
Goal type id (Cornerstone goal_type_id)
startDatedate-time
Start (Cornerstone start_dt)
dueDatedate-time
Due date (Cornerstone due_dt)
completionDatedate-timenot filled
Completion date
weightnumber
Weight (Cornerstone weight)
progressnumber
Progress in percent (Cornerstone progress)
curl "https://api.octo.taxi/v2/goals?limit=200" \
  -H "Authorization: Bearer $OCTOTAXI_API_KEY"

Performance reviews

GET/v2/reviews

Reserved for the Reporting API view vw_rpt_performance_review, which octo.taxi does not sync. It answers 200 with no rows.

Development plans

GET/v2/development-plans

Development plans assigned to people.

Scope
obj_devplan_core:read
Cornerstone
devplan_core (Data Exporter)

Query parameters

limitintegeroptional
Rows per page, 50 by default, at most 200.
cursorstringoptional
The previous answer's meta.cursor.
sortstringoptional
A field name, - in front for descending. Default -dueDate.
fieldsstringoptional
Comma-separated field names to return.
filter[field][op]stringoptional
See Filtering.

Fields

idstring
Unique identifier of the row (Cornerstone devplan_id)
userIdstring
Person (Cornerstone user id) (Cornerstone assigned_user_id)
titlestring
Title (Cornerstone title)
statusstring
Status code as Cornerstone sends it (Cornerstone status_id)
planTypestringnot filled
Plan type
startDatedate-timenot filled
Start
dueDatedate-time
Due date (Cornerstone due_dt)
curl "https://api.octo.taxi/v2/development-plans?limit=200" \
  -H "Authorization: Bearer $OCTOTAXI_API_KEY"

Check-ins

GET/v2/check-ins

Check-in conversations between a person and their manager.

Scope
obj_checkin:read
Cornerstone
checkin_core (Data Exporter)

Query parameters

limitintegeroptional
Rows per page, 50 by default, at most 200.
cursorstringoptional
The previous answer's meta.cursor.
sortstringoptional
A field name, - in front for descending. Default -checkinDate.
fieldsstringoptional
Comma-separated field names to return.
filter[field][op]stringoptional
See Filtering.

Fields

idstring
Unique identifier of the row (Cornerstone checkIn_id)
userIdstring
Person (Cornerstone user id) (Cornerstone checkIn_participantFocusUserId)
managerIdstring
Person who created the check-in (Cornerstone checkIn_createdBy)
checkinDatedate-time
Last activity in the check-in (Cornerstone checkIn_lastActivityDate)
notesstring
Title of the check-in (Cornerstone checkIn_title)
statusstringnot filled
Status code as Cornerstone sends it
curl "https://api.octo.taxi/v2/check-ins?limit=200" \
  -H "Authorization: Bearer $OCTOTAXI_API_KEY"

Competencies

GET/v2/competencies

Reserved for the Reporting API view vw_rpt_competency_local, which octo.taxi does not sync. It answers 200 with no rows.

Assessments

GET/v2/assessments

Assessment evaluations per person.

Scope
obj_assessment_evaluation_core:read
Cornerstone
assessment_evaluation_core (Data Exporter)

Query parameters

limitintegeroptional
Rows per page, 50 by default, at most 200.
cursorstringoptional
The previous answer's meta.cursor.
sortstringoptional
A field name, - in front for descending. Default id.
fieldsstringoptional
Comma-separated field names to return.
filter[field][op]stringoptional
See Filtering.

Fields

idstring
Unique identifier of the row (Cornerstone session_id)
userIdstring
Person (Cornerstone user id) (Cornerstone user_id)
statusstringnot filled
Status code as Cornerstone sends it
overallScorenumbernot filled
Overall score
curl "https://api.octo.taxi/v2/assessments?limit=200" \
  -H "Authorization: Bearer $OCTOTAXI_API_KEY"

Observation checklists

GET/v2/observations

Reserved for the Reporting API view vw_rpt_observation_checklist, which octo.taxi does not sync. It answers 200 with no rows.

Recruiting

Job openings

GET/v2/job-openings

Job requisitions with status, hiring manager and dates.

Scope
obj_job_requisition_core:read
Cornerstone
job_requisition_core (Data Exporter)

Query parameters

limitintegeroptional
Rows per page, 50 by default, at most 200.
cursorstringoptional
The previous answer's meta.cursor.
sortstringoptional
A field name, - in front for descending. Default -postedDate.
fieldsstringoptional
Comma-separated field names to return.
filter[field][op]stringoptional
See Filtering.

Fields

idstring
Unique identifier of the row (Cornerstone requisition_id)
titlestringnot filled
Title
departmentstringnot filled
Name of the person's department OU
locationstringnot filled
Name of the person's location OU
statusstring
Status code as Cornerstone sends it (Cornerstone status_id)
hiringManagerIdstring
Hiring manager (user id) (Cornerstone hiring_manager_id)
postedDatedate-time
Opening date (Cornerstone open_date)
closedDatedate-timenot filled
Closing date
targetHireDatedate-time
Target fill date (Cornerstone target_fill_date)
openingsinteger
Number of openings (Cornerstone original_openings_count)
curl "https://api.octo.taxi/v2/job-openings?limit=200" \
  -H "Authorization: Bearer $OCTOTAXI_API_KEY"

Applications

GET/v2/applications

Job applications with status, source and submission date.

Scope
obj_job_application_core:read
Cornerstone
job_application_core (Data Exporter)

Query parameters

limitintegeroptional
Rows per page, 50 by default, at most 200.
cursorstringoptional
The previous answer's meta.cursor.
sortstringoptional
A field name, - in front for descending. Default -appliedDate.
fieldsstringoptional
Comma-separated field names to return.
filter[field][op]stringoptional
See Filtering.

Fields

idstring
Unique identifier of the row (Cornerstone job_application_id)
requisitionIdstring
Job requisition (Cornerstone job_requisition_id)
candidateNamestringnot filled
Candidate name
candidateEmailstringnot filled
Candidate e-mail
statusstring
Status code as Cornerstone sends it (Cornerstone status_id)
sourcestring
Submission source id (Cornerstone submission_source_id)
appliedDatedate-time
Submission date (Cornerstone submission_dt)
stagestringnot filled
Stage
curl "https://api.octo.taxi/v2/applications?limit=200" \
  -H "Authorization: Bearer $OCTOTAXI_API_KEY"

Offer letters

GET/v2/offers

Reserved for the Reporting API view vw_rpt_offer_letter, which this endpoint does not read. It answers 200 with no rows.

Onboarding

GET/v2/onboarding

Reserved for the Reporting API view vw_rpt_onboarding, which octo.taxi does not sync. It answers 200 with no rows.

Compensation and succession

Succession plans

GET/v2/succession-plans

Reserved for the Reporting API view vw_rpt_smp_job_pool_successor, which octo.taxi does not sync. It answers 200 with no rows.

Temporarily unavailable

These endpoints answer 503 NOT_AVAILABLE until octo.taxi can filter them by reporting line. Read the parent endpoint instead.

/v2/goal-tasks
Use /v2/goals.
/v2/development-actions
Use /v2/development-plans.
/v2/development-objectives
Use /v2/development-plans.
/v2/assessment-results
Use /v2/assessments.

Cornerstone mirror (/v1)

Cornerstone's records exactly as its Data Exporter returns them: the same field names, types, envelope and paging, read from octo.taxi's copy.

Each route returns every field Cornerstone sends for its object (the fields of Cornerstone's Data Exporter dictionary), not the /v2 selection. Parameters: $top, $select, $filter, $skiptoken, filter[field][op] and the header Prefer: odata.maxpagesize=N.

Same path as Cornerstone. The routes also answer under Cornerstone's own path, /services/api/x/dataexporter/api/objects/{object} (and /objects lists what the key may read). An existing Data Exporter integration changes the host to api.octo.taxi and the token to an octo.taxi key; nothing else.

Deltas. octo.taxi keeps no delta token per client: a request with $deltatoken gets the full current data with the headers Preference-Applied: odata.track-changes=false and X-Octotaxi-Delta: full-read, never a fake delta.

More objects. Objects your admins activate beyond this list are served at /v1/{object} to keys with the scope schema:read plus the object's own scope.

One record. GET /v1/users/{user_id} answers one users_core record as { data }, the same fields as a row of /v1/users.

Schema-driven query. POST /v1/query (scope schema:read plus the object's own scope) reads one mirrored table by Cornerstone's field names: body { table, columns?, filters?, sort?, limit?, cursor? } wheretable is the mirror table (users, transcripts, ous ...), columns, filters[].column and sort.column are the object's Data Exporter field names (user_id, user_name_first, transc_user_id ...) and every answer row carries those names; { data, pagination: { cursor, hasMore } }, up to 200 rows a page. Only fields stored in a curated column are served; a table without Data Exporter names (a Reporting API view such as certification holders) answers 400 TABLE_NOT_QUERYABLE and is read through its /v2 endpoint.

curl "https://api.octo.taxi/v1/users?$top=2" \
  -H "Authorization: Bearer $OCTOTAXI_API_KEY"
Response 200 (shortened)
{
  "@odata.context": "/v1/users/$metadata",
  "value": [
    {
      "user_id": 100042,
      "user_ref": "E100042",
      "user_name_first": "Ada",
      "user_name_last": "Example",
      "user_email": "ada.example@example.com",
      "user_status_id": 1,
      "user_mgr_id": 100003,
      "user_hire_dt_orig": "2021-03-15T00:00:00Z"
    }
  ],
  "@odata.nextLink": "/v1/users?$top=2&$skiptoken=eyJpZCI6IjEwMDA0MiJ9"
}

OData feed (/v2/odata)

An OData v4 service over the same rows, fields and privacy as /v2, for BI tools.

/v2/odata
Service document: the entity sets.
/v2/odata/$metadata
The model as CSDL JSON (not XML).
/v2/odata/{EntitySet}
Rows. Entity sets: Employees, OrgUnits, OrgHierarchy, EmployeeAssignments, CustomFields, StatusChanges, Relationships, Learning, LearningObjects, Certifications, CertificationDefinitions.

Query options: $select, $filter (see Filtering), $orderby (one field, asc or desc), $top, $count=true, $skiptoken from @odata.nextLink. Field names are the /v2 names; filters also accept the underlying column names.

Because $metadata is JSON, Power BI's OData connector cannot read it; use the Power BI starter model, which reads the feed with Web.Contents.

curl "https://api.octo.taxi/v2/odata/Learning?$top=200&$count=true&$filter=isRequired%20eq%20true%20and%20exemptDt%20eq%20null" \
  -H "Authorization: Bearer $OCTOTAXI_API_KEY"
Response 200 (shortened)
{
  "@odata.context": "https://api.octo.taxi/v2/odata/$metadata#Learning",
  "value": [
    {
      "id": "100042:3f2c0d1e-0000-4000-8000-000000000001:1",
      "userId": "100042",
      "objectId": "3f2c0d1e-0000-4000-8000-000000000001",
      "status": "64",
      "completionDate": "2026-04-16T00:00:00.000Z",
      "dueDate": "2026-06-15T00:00:00.000Z",
      "score": 99,
      "isRequired": true,
      "exemptDt": null
    }
  ],
  "@odata.count": 1391,
  "@odata.nextLink": "https://api.octo.taxi/v2/odata/Learning?%24skiptoken=...&%24top=200&%24count=true"
}

Point in time (asOf)

?asOf=YYYY-MM-DD answers with the rows as they were at the end of that day (UTC), from octo.taxi's history. Supported on /v2/employees and /v2/employees/{id}.

effectiveFromdate-time
Start of the version of the row (inclusive).
effectiveTodate-time
End of the version, null while it is still current.
meta.asOfstring
The requested day.
meta.temporalCoveragestring
observed (changes octo.taxi has seen) or scheduled-aware (future-dated Cornerstone changes included).
meta.coverageFromdate-time
The earliest day the history covers; earlier days return no rows. Null without history.
400 TEMPORAL_NOT_SUPPORTED
asOf on an endpoint without history.
404 NO_COVERAGE_FOR_DATE
The single record had no version on that day.
Access
Which rows a key may see is decided today, not by the rights on the past day.
curl "https://api.octo.taxi/v2/employees?asOf=2026-01-01&limit=200" \
  -H "Authorization: Bearer $OCTOTAXI_API_KEY"
Response 200 (shortened)
{
  "data": [
    {
      "id": "100042",
      "firstName": "Ada",
      "jobTitle": "Sales Representative",
      "effectiveFrom": "2025-06-01T00:00:00.000Z",
      "effectiveTo": "2026-02-01T00:00:00.000Z"
    }
  ],
  "meta": {
    "total": 254,
    "pageSize": 50,
    "hasMore": true,
    "cursor": "eyJpZCI6IjEwMDA1MCIsInQiOjI1NH0",
    "syncedAt": "2026-10-07T08:30:12.000Z",
    "asOf": "2026-01-01",
    "temporalCoverage": "observed",
    "coverageFrom": "2025-10-08T00:00:00.000Z"
  }
}

Custom endpoints

Your tenant owner can publish custom read endpoints in octo.taxi (System, API): a path over one data source with chosen fields, fixed filters and a default order, served at /v2/custom/{path}.

Same keys, scopes (the data source's scope), entity limits, masking, minimum group size and paging as the built-in /v2 lists, and the same { data, meta } answer. Two formats: simplified (camelCase names you choose, asOf where the source has history) and original (Cornerstone's own field names from an allowlist).

A caller's filters can only narrow the fixed ones. A path that is not published for your tenant answers 404 NOT_FOUND. Sign-ins, compensation and salary data can never be served.

curl "https://api.octo.taxi/v2/custom/active-managers?limit=200" \
  -H "Authorization: Bearer $OCTOTAXI_API_KEY"

Power BI starter model

A ready model for Power BI Desktop on /v2/odata: six tables, their relationships, measures and three report pages (compliance, completions, headcount). It reads with an API key like any other client, so the key decides the tenant, the objects and the entities, and masking and the minimum group size apply as on /v2.

1. Create a key

System, API keys > Create API key (tenant owners), named for example "Power BI". Keep "Read-only (all objects except restricted ones)" or pick exactly the six scopes on the right. Limit it to entities if the report is for one legal entity only. The key is shown once.

2. Paste the queries

  1. 1
    Power BI Desktop > Home > Transform data > New Source > Blank Query.
  2. 2
    For every block in octotaxi-power-query.pq: a new blank query, renamed exactly as the block's // ==== Query: Name ==== line, Advanced Editor, paste the block.
  3. 3
    Start with the parameters OctoTaxiApiUrl and OctoTaxiApiKey (paste your key into the second), then OctoTaxiFeed and OctoTaxiDate, then the tables.
  4. 4
    When Power BI asks how to connect to api.octo.taxi: Web, Anonymous (the key travels in the X-API-Key header), privacy level Organizational.
  5. 5
    Close & Apply. Power BI loads every table page by page.
  • Employees from /v2/odata/Employees: people: name, e-mail, status, hire and termination date, manager, job title, division, department, location. The termination date stays empty unless the owner grants the sensitive category terminations and leave (hr_status) to the admin role, the role every API key reads with
  • Learning from /v2/odata/Learning: transcripts: one row per person and training, with status, due date, completion date, score, the required flag and the exemption date (Mandatory / Exempted / Voluntary)
  • Trainings from /v2/odata/LearningObjects: the training catalogue: title, type, provider
  • OrgUnits from /v2/odata/OrgUnits: organisational units with type and parent
  • EmployeeOrgUnits from /v2/odata/EmployeeAssignments: a person's OU per OU type (the bridge between people and OUs)
  • Certifications from /v2/odata/Certifications: certification holders: one row per person and certification with the status and the dates of the current period
  • Calendar: a date table for the trends (Table tools > Mark as date table).
Scopes
obj_users_core:read
obj_transcript_core:read
obj_training_core:read
obj_ou_core:read
obj_user_ou_core:read
vw_rpt_certification2:read

3. Relationships, measures, pages

octotaxi-model.dax lists the seven relationships (Learning and Certifications to Employees, Learning to Trainings, the OU bridge with cross filter Both, the two date relationships), the measures (completions, mandatory training, headcount, certifications held, expiring in 90 days, expired) and the visuals of the three starter pages. Paste each measure via New measure on the Learning table.

A measure with no matching rows shows "(Blank)" in Power BI where octo.taxi shows 0. That is DAX's way and kept on purpose: a + 0 in the measure would fill every chart and table with zero rows. For a card, add a card-only copy such as Mandatory overdue (card) = [Mandatory overdue] + 0.

Mandatory follows octo.taxi's own rule: Cornerstone's is_required set and no exempt_dt (on the feed isRequired and exemptDt); required with an exemption date is Exempted, everything else Voluntary.

4. Publish and refresh

  1. 1
    Publish to a workspace. Semantic model > Settings > Data source credentials > Edit credentials: Web, authentication Anonymous, privacy level Organizational, and tick Skip test connection. No gateway is needed.
  2. 2
    Why the tick: the service tests the credential with a call to https://api.octo.taxi/ without the key, which answers 401, so without it Power BI rejects Anonymous with "The credentials provided for the Web source are invalid". The refresh itself sends the key and works.
  3. 3
    The key belongs only in the OctoTaxiApiKey parameter in the service, never in the credentials dialog or a .pbix you share.
  4. 4
    Scheduled refresh: up to 8 times a day on Pro, 48 on Premium. octo.taxi syncs every few minutes, so refreshing more often than every 15 minutes adds little.
  5. 5
    Rotate a key: create the new key, paste it into the parameter, refresh once, then revoke the old key.
DAX
Overdue = CALCULATE ( COUNTROWS ( Learning ), Learning[isOverdue] = TRUE () )
Completion rate = DIVIDE ( [Completed], [Assignments] )
Mandatory overdue = CALCULATE ( COUNTROWS ( Learning ), Learning[isMandatory] = TRUE (), Learning[isOverdue] = TRUE () )

When Cornerstone changes

/v1 stores each record as Cornerstone sends it, so a field Cornerstone adds to an object appears on /v1, under Cornerstone's name, on every record octo.taxi reads after the change.

/v2 names are octo.taxi's contract and do not follow a Cornerstone rename. A new /v2 field is named after its Cornerstone field in camelCase (exempt_dt becomes exemptDt).

When Cornerstone deprecates an object, octo.taxi says so in the product and adds no new endpoint on it; the existing endpoint keeps answering with the data synced until the source stops.