Guide
Authentication
Step-by-step OAuth 2.0 setup for the Livepush API — registering an app, the authorization-code flow, scopes, token lifetimes and error codes.
Every Livepush endpoint is protected by the bearerAuth scheme: a JWT in the
Authorization header.
Authorization: Bearer YOUR_API_TOKEN
There is no query-parameter or cookie fallback on the resource APIs. A request without a
valid token returns 401.
Tokens are issued by the auth service at https://tokens.livepush.io, which is
separate from the resource hosts, through the OAuth 2.0 authorization-code flow.
Before you start
Register your application at the developer console. You will get:
client_id |
Public identifier for your app |
client_secret |
Server-side only — never ship it in a browser or mobile bundle |
| Redirect URIs | Where users return after consenting |
A redirect_uri you send during the flow must match one of the registered URIs
exactly. There is no prefix or wildcard matching, so register every callback you use,
including your local development one.
Step 1 — Send the user to consent
GET https://tokens.livepush.io/oauth2/authorize
?client_id=YOUR_CLIENT_ID
&redirect_uri=https://yourapp.example.com/callback
&response_type=code
&scope=streams.read,streams.videos.read
&state=OPAQUE_RANDOM_STRING
response_type must be code. Scopes may be separated by commas or spaces. Generate
state randomly per attempt and store it in the user's session — you verify it in step 3,
and without that check your callback has no CSRF protection.
The user needs an existing Livepush session; this request is authenticated by their srid
session cookie, not by a bearer token. If they are not signed in, send them to Livepush to
sign in first and then start the flow.
This endpoint returns JSON, not an HTML page. Livepush hands you the data and you build the consent screen:
{
"sessionID": "Bk93jd0Ba",
"app": { "name": "Your App", "avatar": "https://…/icon.png" },
"scope": [
{ "title": "streams.read", "description": "streams.read" }
]
}
Show the user which app is asking and which scopes it wants, then collect an approve or
deny. The sessionID is valid for 5 minutes — if the user takes longer, start again.
Step 2 — Post their decision
POST https://tokens.livepush.io/oauth2/authorize
?client_id=YOUR_CLIENT_ID
&redirect_uri=https://yourapp.example.com/callback
&scope=streams.read,streams.videos.read
&state=OPAQUE_RANDOM_STRING
{
"session_id": "Bk93jd0Ba",
"is_rejected": false
}
The response is a 301 back to your redirect URI. On approval it carries the grant code:
https://yourapp.example.com/callback?code=GRANT_CODE&state=OPAQUE_RANDOM_STRING
On refusal or any validation failure it carries an error instead, and no code:
https://yourapp.example.com/callback?error=access_denied&error_description=owner+has+denied+access+to+requested+scope
Step 3 — Verify state, then exchange the code
Compare the returned state against the one you stored. If it does not match, stop — the
callback did not originate from your flow.
curl -X GET 'https://tokens.livepush.io/oauth2/access_token?grant_type=authorization_code&code=GRANT_CODE&client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET&redirect_uri=https://yourapp.example.com/callback'
client_id and redirect_uri must match the values the code was issued against, or the
exchange fails with invalid_request. The code is single use and expires after
10 minutes.
{
"access_token": "eyJhbGciOiJIUzI1NiIs…",
"refresh_token": "V1StGXR8_Z5jdHi6B-myT…",
"token_type": "access_token",
"expires_in": 1209600
}
expires_in is 14 days.
Two things to know about this endpoint. It is a
GETand it takes the client secret as a query parameter, where most OAuth servers use aPOSTbody. Query strings end up in proxy logs, browser history and server access logs — so call it server-side only, never from a browser or mobile client, and make sure your own logging redacts it. Also notetoken_typeisaccess_tokenrather than the conventionalBearer; send the header asAuthorization: Bearer <access_token>regardless of what this field says.
Step 4 — Call the API
curl -X GET 'https://octopus.livepush.io/streams/mine' \
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN'
Each endpoint page in this reference names the scope it requires.
Step 5 — Refresh before day 14
Same endpoint, with grant_type=refresh_token, passing the refresh token in the code
parameter:
curl -X GET 'https://tokens.livepush.io/oauth2/access_token?grant_type=refresh_token&code=YOUR_REFRESH_TOKEN&client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET&redirect_uri=https://yourapp.example.com/callback'
Refresh tokens are single use: the old one is disabled the moment a new pair is issued,
so persist the new refresh_token from every response. Reusing a spent one returns
expired_grant, and the user has to consent again.
What is inside a token
Access tokens are JWTs. The payload carries the account and the granted scopes:
{
"_id": "65f1c2...",
"id": "us_xxxxxxxxxxxx",
"ns": "nsXXXXXXXX",
"name": "Wahaj",
"scope": ["streams.read", "streams.videos.write"]
}
You never need to parse this — but decoding scope locally is a cheap way to check what a
token can do before you spend a call finding out.
Scopes
Ask for the narrowest set that does the job. Every endpoint page names the scope it needs.
| Scope | Grants |
|---|---|
streams.read |
Read streams and their configuration |
streams.write |
Modify streams, including all embed settings |
streams.create |
Create streams |
streams.delete |
Delete streams |
streams.destinations.write |
Add, edit and toggle multistreaming destinations |
streams.videos.read |
Read the video library |
streams.videos.write |
Upload, edit and delete videos |
stream.notifications.read |
Read stream notification settings |
stream.notifications.write |
Change stream notification settings |
players.read |
Read standalone players |
players.write |
Modify players |
players.create |
Create players |
players.delete |
Delete players |
regions.read |
List ingestion and storage regions |
root |
Everything — see below |
root bypasses every check
A token carrying root passes every scope check without the specific scope being present.
Treat it as an account-wide credential: it can delete streams, recordings and videos.
Scopes containing a colon — global:deployments, for instance — are privileged. They are
checked against the account when the token is issued, and a request for one the account
does not hold fails outright.
Error codes
Returned as error on the redirect, or in the body of a failed token exchange.
| Code | Meaning |
|---|---|
invalid_request |
A required parameter is missing, or the consent session expired |
invalid_client |
Unknown client_id, unregistered redirect_uri, or wrong client_secret |
invalid_grant |
The code or refresh token is unknown or already used |
expired_grant |
The refresh token has been retired |
unsupported_response_type |
response_type was not code |
unsupported_grant_type |
grant_type was not recognised |
invalid_scope |
The scope list was empty or unparseable |
access_denied |
The user refused consent |
server_error |
The grant could not be recorded — safe to retry once |
Trick: borrow a token from a logged-in browser tab
When you just want to try an endpoint, standing up the whole OAuth flow is a lot of
ceremony. The dashboard keeps its own access token in localStorage, so a working token is
one line away.
Open app.livepush.io while signed in, open DevTools, and run:
copy(localStorage.getItem('access_token'))
That puts a bearer token on your clipboard. Paste it straight into the samples on these pages:
curl -X GET 'https://squid.livepush.io/account/this' \
-H "Authorization: Bearer $LIVEPUSH_TOKEN"
A few things worth knowing before you rely on it:
- Assume it can do anything to your account. The dashboard needs broad access. Decode
the token and read its
scopeclaim if you want to know exactly what you are holding — but treat it like your password either way. Never paste it into a shared terminal, a ticket, a CI log or a chat message. - It is short-lived. Dashboard tokens expire in about an hour. Re-copy it when calls
start returning
401. That makes this fine for exploring and wrong for anything scheduled. - If you use account switching, read
access_token_aliasinstead. When that key is set the dashboard sends it in preference toaccess_token, and it is the one scoped to the account you are currently looking at.
Use it to explore. For anything that runs more than once, go through the OAuth flow and issue a token with the narrow scopes it needs.
Handling expiry in code
Treat 401 as "get a new token and retry once", never as a permanent failure:
async function call(url, init = {}) {
let response = await fetch(url, withAuth(init));
if (response.status === 401) {
await refreshAccessToken();
response = await fetch(url, withAuth(init));
}
if (!response.ok) {
throw new Error(`Livepush API error ${response.status}`);
}
return response.json();
}
Do not retry more than once. A 401 immediately after a refresh means the credentials
themselves are wrong, and a loop will just hammer the auth service.
Keeping tokens safe
- Keep tokens and the client secret server-side. If a browser needs Livepush data, proxy the call through your own backend rather than shipping a token to the page.
- Never put
client_secretin a mobile app or single-page app bundle — it is not a secret once it ships. - Request the narrowest scopes that work, and avoid
rootin anything automated. - Store refresh tokens encrypted at rest, and replace each one as it is rotated.