---
title: Objectives
description: List, read, create, update and delete the Objectives that measure each Goal.
product: gps
section: api
order: 12
---

# Objectives

An Objective measures progress toward a [Goal](goals.md). 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](check-ins.md).

| Method | Path | Scope | Webhook |
|--------|------|-------|---------|
| `GET` | [`/objectives`](#list-objectives) | `objectives:read` | |
| `GET` | [`/goals/{goalId}/objectives`](#list-a-goals-objectives) | `objectives:read` | |
| `GET` | [`/objectives/{id}`](#get-an-objective) | `objectives:read` | |
| `POST` | [`/objectives`](#create-an-objective) | `objectives:write` | `objective.created` |
| `PATCH` | [`/objectives/{id}`](#update-an-objective) | `objectives:write` | `objective.updated` |
| `DELETE` | [`/objectives/{id}`](#delete-an-objective) | `objectives:write` | `objective.deleted` |

Related: [`GET /objectives/{objectiveId}/actions`](actions.md#list-an-objectives-actions), [check-ins](check-ins.md) and [Objective comments](comments.md).

The JavaScript examples assume a `headers` object holding your key:

```js
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. |

```bash
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"
```

```js
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`**

```json
{
  "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](#list-objectives). |
| `limit`, `offset` | query | See [Pagination](../conventions.md#pagination). |

```bash
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"
```

```js
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](#the-objective-object) with `meta`, as in [List Objectives](#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. |

```bash
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"
```

```js
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](#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](check-ins.md).

**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](#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](../conventions.md#attribution-with-onbehalfof). |

```bash
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"]
  }'
```

```js
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`**

```json
{
  "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](check-ins.md).

**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](#create-an-objective) except `onBehalfOf`, with the same rules. Also:

- Set `parentGoalId` or `parentObjectiveId` to `null` to detach the Objective from that parent.
- `assignedTo` and `invitedUsers` replace the whole list.
- `measurementType` is stored exactly as you send it on update; the common words listed under Create aren't converted.

```bash
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" }'
```

```js
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](#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](#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. |

```bash
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"
```

```js
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.
