Authentication
Create an API key, send it with each request, choose its scopes, and rotate or revoke it.
Every request to the GPS REST API carries an API key. A key belongs to one GPS organization, and every request made with it reads and writes that organization's data only.
Who can create keys#
Members with the Owner, Super Admin or Admin role. The Developer tab in Settings, where keys are managed, is for those roles.
Create a key#
- In GPS, open Settings → Developer → Keys & Webhooks.
- Under API Keys, select New API Key.
- Fill in the form:
- Name (required, up to 100 characters), for example "Production integration"
- Description (optional, up to 500 characters)
- Expires At (optional). After this date the key stops working.
- Scopes (at least one). See Scopes.
- Select Create Key.
GPS then shows two values. Copy both straight away: the secret is shown only once and GPS stores only a one-way hash of it.
| Value | Looks like | Sent as |
|---|---|---|
| API key (key ID) | emp_ followed by 28 characters |
X-API-Key header |
| Secret | A 43-character random string | X-API-Secret header |
The Keys & Webhooks tab also shows your Organization ID. You don't need it to call the API, because the key already identifies your organization.
Send the key#
Use either form. If a request has an X-API-Key header, GPS reads the key from the two X-API-… headers and ignores Authorization.
Two headers (preferred)
X-API-Key: emp_AbCdEfGhIjKlMnOpQrStUvWxYz12
X-API-Secret: <your secret>
Bearer token: the key ID and secret joined by a colon.
Authorization: Bearer emp_AbCdEfGhIjKlMnOpQrStUvWxYz12:<your secret>
curl https://plan.empiraa.app/api/v1/goals \
-H "Authorization: Bearer $GPS_API_KEY_ID:$GPS_API_SECRET"
const res = await fetch('https://plan.empiraa.app/api/v1/goals', {
headers: {
Authorization: `Bearer ${process.env.GPS_API_KEY_ID}:${process.env.GPS_API_SECRET}`,
},
});
Keep the secret on your server. The API accepts requests from browsers (it allows cross-origin requests from any origin), but anyone who can read your page's code can then use the key.
Scopes#
A key can call an endpoint only if it has that endpoint's scope. Choose the fewest scopes your integration needs.
A key's access comes from its scopes alone. It isn't narrowed by the GPS role of the person who created it, and it sees every record in the organization, including Goals, Objectives and Actions marked private in GPS.
| Scope | Lets the key |
|---|---|
organization:read |
GET /organization |
organization:write |
PATCH /organization |
users:read |
GET /users, GET /users/{id} |
users:write |
POST /users, PATCH /users/{id}, DELETE /users/{id} |
goals:read |
GET /goals, GET /goals/{id} |
goals:write |
POST /goals, PATCH /goals/{id}, DELETE /goals/{id} |
objectives:read |
GET /objectives, GET /objectives/{id}, GET /goals/{goalId}/objectives |
objectives:write |
POST /objectives, PATCH /objectives/{id}, DELETE /objectives/{id} |
progress:read |
GET /objectives/{objectiveId}/progress |
progress:write |
POST /objectives/{objectiveId}/progress |
actions:read |
GET /actions, GET /actions/{id}, GET /objectives/{objectiveId}/actions, GET /goals/{goalId}/actions |
actions:write |
POST /actions, PATCH /actions/{id}, DELETE /actions/{id} |
meetings:read |
GET /meetings, GET /meetings/{id} |
meetings:write |
POST /meetings, PATCH /meetings/{id}, DELETE /meetings/{id} |
ideas:read |
GET /ideas, GET /ideas/{id} |
ideas:write |
POST /ideas, PATCH /ideas/{id}, DELETE /ideas/{id} |
comments:read |
GET on the /comments list of a Goal, Objective, Meeting, Action or Idea |
comments:write |
POST on those /comments lists, and every comment DELETE |
GET /api/v1 needs no scope: any valid key can call it to see its own scopes. See API info.
Rotate a key#
GPS has no in-place rotation. To rotate:
- Create a new key with the same scopes.
- Deploy the new key ID and secret to your integration.
- Confirm requests succeed with the new key.
- Revoke the old key.
Use the same approach to change a key's scopes: the settings screen doesn't edit scopes on an existing key.
Revoke a key#
In Settings → Developer → Keys & Webhooks, select the Revoke icon next to the key, then Revoke Key. The key stops working on its next request. Revoked keys stay listed under the active ones for your records; the settings screen can't reactivate them, so create a new key instead.
A key that reaches its Expires At date stops working the same way.
401 and 403 responses#
Authentication failures return the standard error body.
| Status | code |
Meaning |
|---|---|---|
| 401 | MISSING_CREDENTIALS |
No X-API-Key header and no Authorization: Bearer <keyId>:<secret> header. |
| 401 | MISSING_SECRET |
X-API-Key was sent without X-API-Secret. |
| 401 | INVALID_CREDENTIALS |
The key ID doesn't exist, or the secret doesn't match it. |
| 401 | KEY_INACTIVE |
The key has been revoked. |
| 401 | KEY_EXPIRED |
The key is past its expiry date. |
| 403 | INSUFFICIENT_SCOPE |
The key is valid but lacks the endpoint's scope. |
| 403 | ORG_MISMATCH |
The request named a different organization (see Organization isolation). |
{
"error": "This API key does not have the 'goals:write' scope.",
"code": "INSUFFICIENT_SCOPE",
"docs": "https://empiraa.com/docs/api"
}
Who a key acts as#
Records a key creates are attributed to the member who created the key, unless the request names another member with onBehalfOf. See Attribution.

