Docs › API reference › Authentication

API authentication: keys, scopes and expiry

Call the API with an API key in the Authorization header, and know which actions need a signed-in person instead.

Updated

There are two ways to call the API. Scripts and servers use an API key. The OmniPosts web app uses a session cookie. Both act for one organization.

API keys

A key starts with omniposts_. Keys created before OmniPosts was renamed start with jamp_ or nsk_ and keep working until you revoke them.

Send the key as a Bearer token on every request:

A request with no key, or a key that is wrong or revoked, gets 401 with code unauthenticated:

A key needs no CSRF header. If the Authorization header is present and starts with Bearer , OmniPosts uses only the key and ignores any cookie.

curl https://omniposts.dev/v1/channels \
  -H "Authorization: Bearer omniposts_..."
{ "error": { "code": "unauthenticated", "message": "Please sign in." } }

Create a key

Create keys in the app: open Settings, then Agents & API. You can also call the API, but only from a signed-in session (see Person-only actions).

POST /v1/api-keys takes an optional name (blank or missing becomes API key), optional scopes and an optional expiresAt.

The response is 201:

{ "name": "ci", "scopes": ["posts:write", "media:write", "channels:read"], "expiresAt": "2027-01-01T00:00:00Z" }
{
  "id": "0192a3b4-0000-7000-8000-0000000000a1",
  "name": "ci",
  "prefix": "omniposts_Ab12Cd3",
  "key": "omniposts_...",
  "scopes": ["channels:read", "posts:write", "media:write"],
  "expiresAt": "2027-01-01T00:00:00Z"
}

Scopes

A key created without scopes can do everything the account can do through the API (apart from the person-only actions). Give a key for one job only the scopes that job needs: a leaked key then cannot reach the rest.

Scope What it covers
posts:read Posts, their per-channel results, drafts and videos
posts:write Create, check, schedule, edit, cancel, retry and promote posts, drafts and videos
media:read List media and download its bytes
media:write Upload, edit and delete media
channels:read Providers, channels and the state of a connect session
channels:manage Start a connect session; disconnect a channel
metrics:read Metrics and reports
webhooks:manage Webhook endpoints and their deliveries
account:read GET /v1/me, the plan, the posting queue, consents, the API-key list, and the key's own workspace and its members
settings:write Change the plan or the posting queue

A request outside a key's scopes gets 403 with code insufficient_scope; the message names the scope that is missing. Scopes are checked per resource (GET needs the :read scope, anything else the write one), so a new endpoint under a resource you already have is covered. An empty scopes list is refused with 400: leave the field out for a key with every scope.

Expiry

expiresAt must be in the future (else 400). From that moment the key gets 401 unauthenticated, exactly like a revoked key, and it stays in the list with its expiresAt so you can see why. A key without expiresAt never expires.

List and revoke keys

GET /v1/api-keys lists your organization's keys. It shows id, name, prefix (the first characters of the key, so you can recognise it), createdAt and lastUsedAt (null if never used). It never shows the secret.

DELETE /v1/api-keys/{id} revokes a key. The answer is 204. A revoked key gets 401 from then on. An id that is not in your organization gets 404.

The web app signs a person in with POST /v1/auth/login (or POST /v1/auth/signup) and gets a cookie named nsid. It is HttpOnly, SameSite=Lax and Secure, and lasts 30 days. POST /v1/auth/logout ends the session and clears the cookie. Scripts do not need any of this: use a key. See Account.

A browser attaches a cookie to requests that another website triggers. To stop a hostile page from making your browser change your data, every request that changes something (anything other than GET) and is authenticated by the cookie must also send this header:

A cross-site form cannot set that header. Without it you get 403 with code csrf. The sign-up, log-in and log-out endpoints need the header even though they have no cookie yet. The older name X-Neslivis-Client: web is still accepted. Use X-OmniPosts-Client in new code.

This only matters if you call the API with a cookie. A Bearer key never needs the header.

X-OmniPosts-Client: web

Person-only actions

Some actions need a signed-in person, not a key. A key that tries one gets 403 with code forbidden:

These are the person-only operations:

Operation Why
POST /v1/api-keys and DELETE /v1/api-keys/{id} A key must not be able to mint or remove keys
DELETE /v1/channels/{id} Disconnecting destroys tokens that only a person can restore
GET /v1/settings/consents The consent record belongs to a person
GET /v1/settings/export Your whole data set
PUT /v1/plan An organization owner's decision
POST /v1/settings/delete-account Deletes the person who asks
Changing workspaces, members and invites (POST /v1/workspaces and the rest) Who belongs to a workspace is decided by people. See Workspaces and members

PUT /v1/plan also needs the person to be an owner of the organization. A member who is not an owner gets 403 forbidden too. So do renaming or deleting a workspace and managing its members and invites.

Connecting a channel is the one person-only step a key can start: POST /v1/connect-sessions with a key answers a one-time handoffUrl for a person to open. See Start a connect from an API key.

Everything else, including creating posts, uploading media, the queue, reports and webhooks, works with a key.

{ "error": { "code": "forbidden", "message": "..." } }