Objectives
List, read, create, update and delete the Objectives that measure each Goal.
An Objective measures progress toward a Goal. It usually sits directly under a Goal (parentGoalId) or under another Objective as a sub-Objective (parentObjectiveId). To record a new value, use a check-in.
| Method | Path | Scope | Webhook |
|---|---|---|---|
GET |
/objectives |
objectives:read |
|
GET |
/goals/{goalId}/objectives |
objectives:read |
|
GET |
/objectives/{id} |
objectives:read |
|
POST |
/objectives |
objectives:write |
objective.created |
PATCH |
/objectives/{id} |
objectives:write |
objective.updated |
DELETE |
/objectives/{id} |
objectives:write |
objective.deleted |
Related: GET /objectives/{objectiveId}/actions, check-ins and Objective comments.
The JavaScript examples assume a headers object holding your key:
const headers = {
'X-API-Key': process.env.GPS_API_KEY_ID,
'X-API-Secret': process.env.GPS_API_SECRET,
};
The Objective object#
| Field | Type | Notes |
|---|---|---|
id |
string | Objective ID. |
title |
string | |
description |
string or null | |
type |
string | KPI, OKR or ROCK. |
measurementType |
string | How the value is measured: $, %, Number, Status (On Track / Off Track / Complete), or the name of a measurement type your organization defined. |
aggregationType |
string or null | How child values combine (cumulative or average), when set. |
progress |
string | Progress percentage as a decimal string. Can exceed 100 when a target is beaten. |
status |
string or null | not_started, started, on_track, off_track or completed. null means GPS works the status out from progress and dates. |
startDate |
string or null | ISO 8601 timestamp. |
dueDate |
string or null | ISO 8601 timestamp. |
completedDate |
string or null | ISO 8601 timestamp. |
targetValue |
number or null | The value that means 100%. |
currentValue |
number or null | The latest recorded value. |
unit |
string or null | Unit label, for example customers. |
assignedTo |
string[] | Member IDs of the people responsible. |
isPrivate |
boolean | Private in GPS. API keys still see private Objectives. |
invitedUsers |
string[] | Member IDs who can see the Objective when it's private. |
parentGoalId |
string or null | The Goal it sits under. |
parentObjectiveId |
string or null | The Objective it sits under, for a sub-Objective. |
checkinFrequency |
string or null | weekly, monthly or quarterly. |
checkinDay |
integer or null | Day of the week (1–7) for weekly check-ins, or day of the month (1–31) for monthly and quarterly ones. |
trackingPeriod |
string | total, weekly, monthly or yearly: whether progress is measured over the whole Objective or resets each period. |
lastResetDate |
string or null | When progress last reset for a periodic trackingPeriod. |
lastPeriodProgress |
string or null | The progress the last finished period ended on. |
archived |
boolean | Archived Objectives are hidden from most endpoints, so this is usually false. |
priority |
string or null | High, Medium or Low. |
headerImageUrl |
string or null | Display setting in GPS. |
headerColor |
string or null | Display setting in GPS. |
calculationMode |
string | manual (progress is recorded) or auto (progress is calculated from sub-Objectives). |
isRollupLocked |
boolean | true when progress is read-only because it's calculated from sub-Objectives. |
aggregatedProgress |
string or null | Progress rolled up from sub-Objectives. |
integrationFormula |
object or null | Settings for updating this Objective from a connected integration. Read-only through the API. |
lastIntegrationSync |
string or null | When a connected integration last updated it. |
integrationSyncStatus |
string or null | active, paused or error. |
autoSyncEnabled |
boolean | Whether a connected integration updates it automatically. |
sortOrder |
integer | Position within its Goal. |
weight |
string or null | Share of the Goal's progress (0–100). null means an equal share. |
createdBy |
string or null | Member ID of the creator. |
organizationId |
string | |
sourceMeetingId |
string or null | The Meeting it was created in, if any. |
createdAt |
string | ISO 8601 timestamp. |
updatedAt |
string | ISO 8601 timestamp. |
List Objectives#
GET /objectives
Returns the organization's Objectives that aren't archived, in GPS order (by sortOrder, then oldest first).
Scope: objectives:read
Query parameters
| Parameter | Type | Notes |
|---|---|---|
status |
string | One of not_started, started, on_track, off_track, completed. at_risk and missed mean off_track; active applies no filter; archived returns 400. |
goalId |
string | Only Objectives directly under this Goal. |
limit |
integer | 1–200, default 50. |
offset |
integer | Default 0. |
curl "https://plan.empiraa.app/api/v1/objectives?goalId=a1f3c5e7-2b4d-4f6a-8c9e-0b1d3f5a7c9e&status=off_track" \
-H "X-API-Key: $GPS_API_KEY_ID" \
-H "X-API-Secret: $GPS_API_SECRET"
const params = new URLSearchParams({ goalId: 'a1f3c5e7-2b4d-4f6a-8c9e-0b1d3f5a7c9e', status: 'off_track' });
const res = await fetch(`https://plan.empiraa.app/api/v1/objectives?${params}`, { headers });
const { data: objectives, meta } = await res.json();
Response 200
{
"data": [
{
"id": "c7e9a1b3-4d5f-4a6b-9c8d-1e2f3a4b5c6d",
"title": "Reach $120k monthly recurring revenue",
"description": null,
"type": "KPI",
"measurementType": "$",
"aggregationType": null,
"progress": "45",
"status": "off_track",
"startDate": "2026-07-01T00:00:00.000Z",
"dueDate": "2027-06-30T00:00:00.000Z",
"completedDate": null,
"targetValue": 120000,
"currentValue": 54000,
"unit": null,
"assignedTo": ["5d7f9b1d-3e5a-4c7e-9f1a-2b3c4d5e6f70"],
"isPrivate": false,
"invitedUsers": [],
"parentGoalId": "a1f3c5e7-2b4d-4f6a-8c9e-0b1d3f5a7c9e",
"parentObjectiveId": null,
"checkinFrequency": "monthly",
"checkinDay": 1,
"trackingPeriod": "total",
"lastResetDate": null,
"lastPeriodProgress": null,
"archived": false,
"priority": "High",
"headerImageUrl": null,
"headerColor": "#1e293b",
"calculationMode": "manual",
"isRollupLocked": false,
"aggregatedProgress": null,
"integrationFormula": null,
"lastIntegrationSync": null,
"integrationSyncStatus": null,
"autoSyncEnabled": false,
"sortOrder": 0,
"weight": null,
"createdBy": "5d7f9b1d-3e5a-4c7e-9f1a-2b3c4d5e6f70",
"organizationId": "9b2f4e1a-6c3d-4e8f-a1b2-c3d4e5f6a7b8",
"sourceMeetingId": null,
"createdAt": "2026-06-24T01:10:00.000Z",
"updatedAt": "2026-11-01T00:00:00.000Z"
}
],
"meta": { "total": 1, "limit": 50, "offset": 0 }
}
Errors: 400 INVALID_STATUS_QUERY for an unknown status; 422 VALIDATION_ERROR for a bad limit or offset.
List a Goal's Objectives#
GET /goals/{goalId}/objectives
Returns the Objectives directly under a Goal (not their sub-Objectives). Unlike GET /objectives, this list can include archived Objectives, so check archived. Order isn't guaranteed.
Scope: objectives:read
| Parameter | In | Notes |
|---|---|---|
goalId |
path | Goal ID. |
status |
query | Same values as List Objectives. |
limit, offset |
query | See Pagination. |
curl https://plan.empiraa.app/api/v1/goals/a1f3c5e7-2b4d-4f6a-8c9e-0b1d3f5a7c9e/objectives \
-H "X-API-Key: $GPS_API_KEY_ID" \
-H "X-API-Secret: $GPS_API_SECRET"
const res = await fetch(
'https://plan.empiraa.app/api/v1/goals/a1f3c5e7-2b4d-4f6a-8c9e-0b1d3f5a7c9e/objectives',
{ headers },
);
const { data: objectives, meta } = await res.json();
Response 200: a list of Objective objects with meta, as in List Objectives.
Errors: 404 NOT_FOUND if the Goal doesn't exist, is in another organization or is archived; 400 INVALID_STATUS_QUERY; 422 VALIDATION_ERROR for a bad limit or offset.
Get an Objective#
GET /objectives/{id}
Scope: objectives:read
| Path parameter | Notes |
|---|---|
id |
Objective ID. |
curl https://plan.empiraa.app/api/v1/objectives/c7e9a1b3-4d5f-4a6b-9c8d-1e2f3a4b5c6d \
-H "X-API-Key: $GPS_API_KEY_ID" \
-H "X-API-Secret: $GPS_API_SECRET"
const res = await fetch('https://plan.empiraa.app/api/v1/objectives/c7e9a1b3-4d5f-4a6b-9c8d-1e2f3a4b5c6d', { headers });
const { data: objective } = await res.json();
Response 200: { "data": <Objective> }, with the same fields as the item in List Objectives.
Errors: 404 NOT_FOUND if the Objective doesn't exist, is in another organization, or is archived.
Create an Objective#
POST /objectives
Neither parent is required, but an Objective can't have both a parentGoalId and a parentObjectiveId.
progress isn't calculated from currentValue when you create an Objective. To set a value and have GPS work out progress from the target, create the Objective, then record a check-in.
Scope: objectives:write · Webhook: objective.created, with the new Objective as data.
Request body
| Field | Type | Required | Allowed values | Notes |
|---|---|---|---|---|
title |
string | Yes | At least 1 character after trimming | |
description |
string or null | No | ||
parentGoalId |
string or null | No | A Goal in your organization | Not with parentObjectiveId. |
parentObjectiveId |
string or null | No | An Objective in your organization | Not with parentGoalId. |
type |
string | No | KPI, OKR, ROCK |
Default OKR. |
measurementType |
string | No | $, %, Number, Status (On Track / Off Track / Complete), or one of your organization's measurement types |
Default Number. Common words are recognized: currency, usd, money become $; percent, percentage become %; count becomes Number; status, binary, boolean become the status type. |
status |
string | No | not_started, started, on_track, off_track, completed |
Default not_started. |
progress |
number or string | No | 0–1000 | Percentage. Default 0. |
priority |
string or null | No | High, Medium, Low |
|
targetValue |
number or null | No | Greater than 0 | |
currentValue |
number, string or null | No | A string has $, ,, % and spaces removed before it's read as a number. |
|
unit |
string or null | No | For example customers. |
|
startDate |
string or null | No | ISO 8601 | |
dueDate |
string or null | No | ISO 8601 | On or after startDate. |
completedDate |
string or null | No | ISO 8601 | |
assignedTo |
string[] | No | Active member IDs | Default []. |
isPrivate |
boolean | No | Default false. |
|
invitedUsers |
string[] | No | Member IDs | Default []. |
checkinFrequency |
string or null | No | weekly, monthly, quarterly |
|
checkinDay |
integer or null | No | 1–31 | See checkinDay in the Objective object. |
trackingPeriod |
string | No | total, weekly, monthly, yearly |
Default total. |
onBehalfOf |
string | No | Active member ID | Who the Objective is created by. See Attribution. |
curl -X POST https://plan.empiraa.app/api/v1/objectives \
-H "X-API-Key: $GPS_API_KEY_ID" \
-H "X-API-Secret: $GPS_API_SECRET" \
-H "Content-Type: application/json" \
-d '{
"title": "Reach $120k monthly recurring revenue",
"parentGoalId": "a1f3c5e7-2b4d-4f6a-8c9e-0b1d3f5a7c9e",
"type": "KPI",
"measurementType": "currency",
"targetValue": 120000,
"checkinFrequency": "monthly",
"checkinDay": 1,
"assignedTo": ["5d7f9b1d-3e5a-4c7e-9f1a-2b3c4d5e6f70"]
}'
const res = await fetch('https://plan.empiraa.app/api/v1/objectives', {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({
title: 'Reach $120k monthly recurring revenue',
parentGoalId: 'a1f3c5e7-2b4d-4f6a-8c9e-0b1d3f5a7c9e',
type: 'KPI',
measurementType: 'currency',
targetValue: 120000,
checkinFrequency: 'monthly',
checkinDay: 1,
assignedTo: ['5d7f9b1d-3e5a-4c7e-9f1a-2b3c4d5e6f70'],
}),
});
const { data: objective } = await res.json();
Response 201
{
"data": {
"id": "c7e9a1b3-4d5f-4a6b-9c8d-1e2f3a4b5c6d",
"title": "Reach $120k monthly recurring revenue",
"description": null,
"type": "KPI",
"measurementType": "$",
"aggregationType": null,
"progress": "0",
"status": "not_started",
"startDate": null,
"dueDate": null,
"completedDate": null,
"targetValue": 120000,
"currentValue": null,
"unit": null,
"assignedTo": ["5d7f9b1d-3e5a-4c7e-9f1a-2b3c4d5e6f70"],
"isPrivate": false,
"invitedUsers": [],
"parentGoalId": "a1f3c5e7-2b4d-4f6a-8c9e-0b1d3f5a7c9e",
"parentObjectiveId": null,
"checkinFrequency": "monthly",
"checkinDay": 1,
"trackingPeriod": "total",
"lastResetDate": null,
"lastPeriodProgress": null,
"archived": false,
"priority": null,
"headerImageUrl": null,
"headerColor": "#1e293b",
"calculationMode": "manual",
"isRollupLocked": false,
"aggregatedProgress": null,
"integrationFormula": null,
"lastIntegrationSync": null,
"integrationSyncStatus": null,
"autoSyncEnabled": false,
"sortOrder": 0,
"weight": null,
"createdBy": "5d7f9b1d-3e5a-4c7e-9f1a-2b3c4d5e6f70",
"organizationId": "9b2f4e1a-6c3d-4e8f-a1b2-c3d4e5f6a7b8",
"sourceMeetingId": null,
"createdAt": "2026-11-12T03:16:05.300Z",
"updatedAt": "2026-11-12T03:16:05.300Z"
}
}
Errors
| Status | code |
When |
|---|---|---|
| 422 | VALIDATION_ERROR |
Missing title; both parents sent; targetValue not above 0; checkinDay outside 1–31; a value outside the allowed list; dueDate before startDate. |
| 422 | INVALID_REFERENCE |
parentGoalId or parentObjectiveId isn't in your organization (or is archived). |
| 422 | INVALID_USERS |
An assignedTo or onBehalfOf ID isn't an active member. |
| 422 | NO_KEY_OWNER |
No onBehalfOf, and the key's creator is no longer a member. |
Update an Objective#
PATCH /objectives/{id}
Changes only the fields you send. A body with no recognized fields changes nothing, returns the Objective as it is, and fires no webhook.
Changing currentValue here doesn't recalculate progress. To record a new value with its progress, use a check-in.
Scope: objectives:write · Webhook: objective.updated, with the updated Objective as data.
| Path parameter | Notes |
|---|---|
id |
Objective ID. |
Request body: any field from Create an Objective except onBehalfOf, with the same rules. Also:
- Set
parentGoalIdorparentObjectiveIdtonullto detach the Objective from that parent. assignedToandinvitedUsersreplace the whole list.measurementTypeis stored exactly as you send it on update; the common words listed under Create aren't converted.
curl -X PATCH https://plan.empiraa.app/api/v1/objectives/c7e9a1b3-4d5f-4a6b-9c8d-1e2f3a4b5c6d \
-H "X-API-Key: $GPS_API_KEY_ID" \
-H "X-API-Secret: $GPS_API_SECRET" \
-H "Content-Type: application/json" \
-d '{ "status": "on_track", "dueDate": "2027-03-31" }'
const res = await fetch('https://plan.empiraa.app/api/v1/objectives/c7e9a1b3-4d5f-4a6b-9c8d-1e2f3a4b5c6d', {
method: 'PATCH',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ status: 'on_track', dueDate: '2027-03-31' }),
});
const { data: objective } = await res.json();
Response 200: { "data": <Objective> } with the changes applied, for example "status": "on_track" and "dueDate": "2027-03-31T00:00:00.000Z". Every other field is as in List Objectives.
Errors: 404 NOT_FOUND if the Objective doesn't exist, is in another organization or is archived; otherwise the same 422 errors as Create an Objective.
Delete an Objective#
DELETE /objectives/{id}
Permanently deletes the Objective and everything under it: its sub-Objectives, its Actions, its check-in history and its comments.
Scope: objectives:write · Webhook: objective.deleted, with data set to { "id": "<objective id>" }. Sub-Objectives and Actions removed with it don't fire their own events.
| Path parameter | Notes |
|---|---|
id |
Objective ID. |
curl -X DELETE https://plan.empiraa.app/api/v1/objectives/c7e9a1b3-4d5f-4a6b-9c8d-1e2f3a4b5c6d \
-H "X-API-Key: $GPS_API_KEY_ID" \
-H "X-API-Secret: $GPS_API_SECRET"
const res = await fetch('https://plan.empiraa.app/api/v1/objectives/c7e9a1b3-4d5f-4a6b-9c8d-1e2f3a4b5c6d', {
method: 'DELETE',
headers,
});
console.log(res.status); // 204
Response 204: no body.
Errors: 404 NOT_FOUND if the Objective doesn't exist, is in another organization, or is archived.

