---
title: Goals
description: List, read, create, update and delete the Goals at the top of your organization's strategy.
product: gps
section: api
order: 11
---

# Goals

A Goal is a top-level outcome in your organization's strategy. [Objectives](objectives.md) measure it and [Actions](actions.md) move it forward.

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

Related: [`GET /goals/{goalId}/objectives`](objectives.md#list-a-goals-objectives), [`GET /goals/{goalId}/actions`](actions.md#list-a-goals-actions) and [Goal 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 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](../conventions.md#pagination). |
| `offset` | integer | Default 0. |

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

```js
const res = await fetch('https://plan.empiraa.app/api/v1/goals?status=on_track', { headers });
const { data: goals, meta } = await res.json();
```

**Response `200`**

```json
{
  "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. |

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

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

```json
{
  "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](../conventions.md#attribution-with-onbehalfof). |

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

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

```json
{
  "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.

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

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

```json
{
  "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. |

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

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