Goals
List, read, create, update and delete the Goals at the top of your organization's strategy.
A Goal is a top-level outcome in your organization's strategy. Objectives measure it and Actions move it forward.
| Method | Path | Scope | Webhook |
|---|---|---|---|
GET |
/goals |
goals:read |
|
GET |
/goals/{id} |
goals:read |
|
POST |
/goals |
goals:write |
goal.created |
PATCH |
/goals/{id} |
goals:write |
goal.updated |
DELETE |
/goals/{id} |
goals:write |
goal.deleted |
Related: GET /goals/{goalId}/objectives, GET /goals/{goalId}/actions and Goal 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 Goal object#
| Field | Type | Notes |
|---|---|---|
id |
string | Goal ID. |
title |
string | |
description |
string or null | |
type |
string | KPI, OKR or ROCK. |
progress |
string | Progress percentage as a decimal string, for example "45". Can exceed 100. |
status |
string or null | not_started, started, on_track, off_track or completed. |
startDate |
string or null | ISO 8601 timestamp. |
dueDate |
string or null | ISO 8601 timestamp. |
completedDate |
string or null | ISO 8601 timestamp. |
assignedTo |
string[] | Member IDs of the people responsible. |
isPrivate |
boolean | Private in GPS. API keys still see private Goals. |
invitedUsers |
string[] | Member IDs who can see the Goal when it's private. |
archived |
boolean | Archived Goals aren't returned by this API, so this is false in responses. |
priority |
string or null | High, Medium or Low. |
color |
string or null | Accent color as a hex value, for example "#3b82f6". |
sortOrder |
integer | Position in the organization's Goal list. |
createdBy |
string or null | Member ID of the creator. |
organizationId |
string | |
sourceMeetingId |
string or null | The Meeting the Goal was created in, if any. |
createdAt |
string | ISO 8601 timestamp. |
updatedAt |
string | ISO 8601 timestamp. |
List Goals#
GET /goals
Returns the organization's Goals that aren't archived, completed ones included, in the order they appear in GPS (by sortOrder, then oldest first).
Scope: goals:read
Query parameters
| Parameter | Type | Notes |
|---|---|---|
status |
string | One of not_started, started, on_track, off_track, completed. Older values are still accepted: at_risk and missed mean off_track, and active applies no filter. archived returns 400. |
limit |
integer | 1–200, default 50. See Pagination. |
offset |
integer | Default 0. |
curl "https://plan.empiraa.app/api/v1/goals?status=on_track" \
-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?status=on_track', { headers });
const { data: goals, meta } = await res.json();
Response 200
{
"data": [
{
"id": "a1f3c5e7-2b4d-4f6a-8c9e-0b1d3f5a7c9e",
"title": "Grow recurring revenue",
"description": "Lift monthly recurring revenue across every service line.",
"type": "OKR",
"progress": "45",
"status": "on_track",
"startDate": "2026-07-01T00:00:00.000Z",
"dueDate": "2027-06-30T00:00:00.000Z",
"completedDate": null,
"assignedTo": ["5d7f9b1d-3e5a-4c7e-9f1a-2b3c4d5e6f70"],
"isPrivate": false,
"invitedUsers": [],
"archived": false,
"priority": "High",
"color": "#3b82f6",
"sortOrder": 0,
"createdBy": "5d7f9b1d-3e5a-4c7e-9f1a-2b3c4d5e6f70",
"organizationId": "9b2f4e1a-6c3d-4e8f-a1b2-c3d4e5f6a7b8",
"sourceMeetingId": null,
"createdAt": "2026-06-24T01:02:03.456Z",
"updatedAt": "2026-10-30T22:45:10.120Z"
}
],
"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.
Get a Goal#
GET /goals/{id}
Scope: goals:read
| Path parameter | Notes |
|---|---|
id |
Goal ID. |
curl https://plan.empiraa.app/api/v1/goals/a1f3c5e7-2b4d-4f6a-8c9e-0b1d3f5a7c9e \
-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', { headers });
const { data: goal } = await res.json();
Response 200
{
"data": {
"id": "a1f3c5e7-2b4d-4f6a-8c9e-0b1d3f5a7c9e",
"title": "Grow recurring revenue",
"description": "Lift monthly recurring revenue across every service line.",
"type": "OKR",
"progress": "45",
"status": "on_track",
"startDate": "2026-07-01T00:00:00.000Z",
"dueDate": "2027-06-30T00:00:00.000Z",
"completedDate": null,
"assignedTo": ["5d7f9b1d-3e5a-4c7e-9f1a-2b3c4d5e6f70"],
"isPrivate": false,
"invitedUsers": [],
"archived": false,
"priority": "High",
"color": "#3b82f6",
"sortOrder": 0,
"createdBy": "5d7f9b1d-3e5a-4c7e-9f1a-2b3c4d5e6f70",
"organizationId": "9b2f4e1a-6c3d-4e8f-a1b2-c3d4e5f6a7b8",
"sourceMeetingId": null,
"createdAt": "2026-06-24T01:02:03.456Z",
"updatedAt": "2026-10-30T22:45:10.120Z"
}
}
Errors: 404 NOT_FOUND if the Goal doesn't exist, is in another organization, or is archived.
Create a Goal#
POST /goals
Scope: goals:write · Webhook: goal.created, with the new Goal as data.
Request body
| Field | Type | Required | Allowed values | Notes |
|---|---|---|---|---|
title |
string | Yes | At least 1 character after trimming | |
description |
string or null | No | ||
type |
string | No | KPI, OKR, ROCK |
Default OKR. Can't be changed later through the API. |
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 |
|
startDate |
string or null | No | ISO 8601 date or date-time | |
dueDate |
string or null | No | ISO 8601 date or date-time | On or after startDate. |
completedDate |
string or null | No | ISO 8601 date or date-time | |
assignedTo |
string[] | No | Active member IDs | Default []. |
isPrivate |
boolean | No | Default false. |
|
invitedUsers |
string[] | No | Member IDs | Default []. |
onBehalfOf |
string | No | Active member ID | Who the Goal is created by. Default: the key's creator. See Attribution. |
curl -X POST https://plan.empiraa.app/api/v1/goals \
-H "X-API-Key: $GPS_API_KEY_ID" \
-H "X-API-Secret: $GPS_API_SECRET" \
-H "Content-Type: application/json" \
-d '{
"title": "Grow recurring revenue",
"type": "OKR",
"priority": "High",
"startDate": "2026-07-01",
"dueDate": "2027-06-30",
"assignedTo": ["5d7f9b1d-3e5a-4c7e-9f1a-2b3c4d5e6f70"]
}'
const res = await fetch('https://plan.empiraa.app/api/v1/goals', {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({
title: 'Grow recurring revenue',
type: 'OKR',
priority: 'High',
startDate: '2026-07-01',
dueDate: '2027-06-30',
assignedTo: ['5d7f9b1d-3e5a-4c7e-9f1a-2b3c4d5e6f70'],
}),
});
const { data: goal } = await res.json();
Response 201
{
"data": {
"id": "a1f3c5e7-2b4d-4f6a-8c9e-0b1d3f5a7c9e",
"title": "Grow recurring revenue",
"description": null,
"type": "OKR",
"progress": "0",
"status": "not_started",
"startDate": "2026-07-01T00:00:00.000Z",
"dueDate": "2027-06-30T00:00:00.000Z",
"completedDate": null,
"assignedTo": ["5d7f9b1d-3e5a-4c7e-9f1a-2b3c4d5e6f70"],
"isPrivate": false,
"invitedUsers": [],
"archived": false,
"priority": "High",
"color": null,
"sortOrder": 0,
"createdBy": "5d7f9b1d-3e5a-4c7e-9f1a-2b3c4d5e6f70",
"organizationId": "9b2f4e1a-6c3d-4e8f-a1b2-c3d4e5f6a7b8",
"sourceMeetingId": null,
"createdAt": "2026-11-12T03:15:42.118Z",
"updatedAt": "2026-11-12T03:15:42.118Z"
}
}
Errors
| Status | code |
When |
|---|---|---|
| 422 | VALIDATION_ERROR |
Missing title, a value outside the allowed list, progress outside 0–1000, or dueDate before startDate. |
| 422 | INVALID_USERS |
An assignedTo or onBehalfOf ID isn't an active member. The body lists invalidIds. |
| 422 | NO_KEY_OWNER |
No onBehalfOf, and the key's creator is no longer a member. |
Update a Goal#
PATCH /goals/{id}
Changes only the fields you send. A body with no recognized fields changes nothing, returns the Goal as it is, and fires no webhook.
Scope: goals:write · Webhook: goal.updated, with the updated Goal as data.
| Path parameter | Notes |
|---|---|
id |
Goal ID. |
Request body: any of these fields.
| Field | Type | Allowed values | Notes |
|---|---|---|---|
title |
string | At least 1 character after trimming | |
description |
string or null | ||
status |
string | not_started, started, on_track, off_track, completed |
|
progress |
number or string | 0–1000 | Percentage. |
priority |
string or null | High, Medium, Low |
|
startDate |
string or null | ISO 8601 | |
dueDate |
string or null | ISO 8601 | If you send both dates, dueDate must be on or after startDate. |
completedDate |
string or null | ISO 8601 | |
assignedTo |
string[] | Active member IDs | Replaces the whole list. |
isPrivate |
boolean | ||
invitedUsers |
string[] | Member IDs | Replaces the whole list. |
type and onBehalfOf are ignored on update.
curl -X PATCH https://plan.empiraa.app/api/v1/goals/a1f3c5e7-2b4d-4f6a-8c9e-0b1d3f5a7c9e \
-H "X-API-Key: $GPS_API_KEY_ID" \
-H "X-API-Secret: $GPS_API_SECRET" \
-H "Content-Type: application/json" \
-d '{ "status": "off_track", "progress": 38 }'
const res = await fetch('https://plan.empiraa.app/api/v1/goals/a1f3c5e7-2b4d-4f6a-8c9e-0b1d3f5a7c9e', {
method: 'PATCH',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ status: 'off_track', progress: 38 }),
});
const { data: goal } = await res.json();
Response 200
{
"data": {
"id": "a1f3c5e7-2b4d-4f6a-8c9e-0b1d3f5a7c9e",
"title": "Grow recurring revenue",
"description": "Lift monthly recurring revenue across every service line.",
"type": "OKR",
"progress": "38",
"status": "off_track",
"startDate": "2026-07-01T00:00:00.000Z",
"dueDate": "2027-06-30T00:00:00.000Z",
"completedDate": null,
"assignedTo": ["5d7f9b1d-3e5a-4c7e-9f1a-2b3c4d5e6f70"],
"isPrivate": false,
"invitedUsers": [],
"archived": false,
"priority": "High",
"color": "#3b82f6",
"sortOrder": 0,
"createdBy": "5d7f9b1d-3e5a-4c7e-9f1a-2b3c4d5e6f70",
"organizationId": "9b2f4e1a-6c3d-4e8f-a1b2-c3d4e5f6a7b8",
"sourceMeetingId": null,
"createdAt": "2026-06-24T01:02:03.456Z",
"updatedAt": "2026-10-30T22:45:10.120Z"
}
}
Errors
| Status | code |
When |
|---|---|---|
| 404 | NOT_FOUND |
The Goal doesn't exist, is in another organization, or is archived. |
| 422 | VALIDATION_ERROR |
A value outside the allowed list or range, or dueDate before startDate. |
| 422 | INVALID_USERS |
An assignedTo ID isn't an active member. |
Delete a Goal#
DELETE /goals/{id}
Permanently deletes the Goal and everything under it: its Objectives and their sub-Objectives, the Actions linked to the Goal or those Objectives, their check-in history and their comments.
Scope: goals:write · Webhook: goal.deleted, with data set to { "id": "<goal id>" }. The Objectives and Actions removed with it don't fire their own deleted events.
| Path parameter | Notes |
|---|---|
id |
Goal ID. |
curl -X DELETE https://plan.empiraa.app/api/v1/goals/a1f3c5e7-2b4d-4f6a-8c9e-0b1d3f5a7c9e \
-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', {
method: 'DELETE',
headers,
});
console.log(res.status); // 204
Response 204: no body.
Errors: 404 NOT_FOUND if the Goal doesn't exist, is in another organization, or is archived.

