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.
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 session cookie
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.
Why cookie writes need X-OmniPosts-Client: web
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": "..." } }
Related
- Errors for the
401and403envelopes. - Rate limits for the limits on
/v1/auth/*.