Building Apps with the Evercam OAuth API

Quick start: Register your app

Developer Portal

If you have a regular Evercam account, use the developer portal to register a new app:



Create an app by filling in the required fields: app name, redirect URL, and required scopes



The response includes client_id and client_secret. Save the secret — it's only shown once. you can regenerate a new client secrets later.


For public clients (SPAs, mobile apps — anything where you can't keep a secret), add "is_public": true. Public clients won't receive a client_secret and must use PKCE (see below).


Authorisation Code Flow (recommended for web apps)

This is the standard OAuth 2.0 flow. Use it for any app with a backend that can securely store a client_secret.

Step 1: Redirect the user to authorise

POST https://media.evercam.io/oauth/authorize
  ?client_id=ev_YOUR_CLIENT_ID
  &redirect_uri=https://your-app.vercel.app/callback
  &response_type=code
  &scope=cameras:read camera:live_view
  &state=random-csrf-token

The user sees a consent screen dash.evercam.io listing the permissions you requested. If they approve, they're redirected back:

https://your-app.vercel.app/callback?code=AUTH_CODE&state=random-csrf-token

Always verify state matches what you sent to prevent CSRF attacks.

Step 2: Exchange code for tokens

curl -X POST https://media.evercam.io/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "authorization_code",
    "client_id": "ev_YOUR_CLIENT_ID",
    "client_secret": "evs_YOUR_SECRET",
    "code": "AUTH_CODE",
    "redirect_uri": "https://your-app.vercel.app/callback"
  }'

Response:

{
  "access_token": "eyJ...",
  "refresh_token": "eyJ...",
  "token_type": "Bearer",
  "expires_in": 3600
}

Step 3: Call the API

curl https://media.evercam.io/v2/cameras \
  -H "Authorization: Bearer ACCESS_TOKEN"

The token only has access to endpoints covered by the scopes you requested. Anything else returns 403 Forbidden.

Step 4: Refresh when expired

Access tokens expire after 1 hour. Use the refresh token to get a new pair:

curl -X POST https://media.evercam.io/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "refresh_token",
    "client_id": "ev_YOUR_CLIENT_ID",
    "client_secret": "evs_YOUR_SECRET",
    "refresh_token": "REFRESH_TOKEN"
  }'

Refresh tokens are rotated — each refresh gives you a new refresh token and invalidates the old one. Refresh tokens are valid for 30 days.


Authorisation Code Flow with PKCE (for SPAs and mobile apps)

If your app runs entirely in the browser (React, Next.js frontend-only, etc.) or is a native mobile app, you can't safely store a client_secret. Use PKCE instead.

Register your app as a public client ("is_public": true).

Step 1: Generate PKCE challenge

// Generate a random code_verifier (43-128 chars, URL-safe)
function generateVerifier() {
  const array = new Uint8Array(32);
  crypto.getRandomValues(array);
  return btoa(String.fromCharCode(...array))
    .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

// Derive code_challenge from verifier using SHA-256
async function generateChallenge(verifier) {
  const encoder = new TextEncoder();
  const data = encoder.encode(verifier);
  const digest = await crypto.subtle.digest('SHA-256', data);
  return btoa(String.fromCharCode(...new Uint8Array(digest)))
    .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

const codeVerifier = generateVerifier();
const codeChallenge = await generateChallenge(codeVerifier);
// Store codeVerifier in sessionStorage — you'll need it in Step 2
sessionStorage.setItem('pkce_verifier', codeVerifier);

Step 2: Redirect to authorise (with PKCE params)

POST https://media.evercam.io/oauth/authorize
  ?client_id=ev_YOUR_CLIENT_ID
  &redirect_uri=https://your-app.vercel.app/callback
  &response_type=code
  &scope=cameras:read camera:live_view
  &state=random-csrf-token
  &code_challenge=CODE_CHALLENGE
  &code_challenge_method=S256

Step 3: Exchange code (with verifier, no secret)

curl -X POST https://media.evercam.io/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "authorization_code",
    "client_id": "ev_YOUR_CLIENT_ID",
    "code": "AUTH_CODE",
    "redirect_uri": "https://your-app.vercel.app/callback",
    "code_verifier": "THE_ORIGINAL_VERIFIER"
  }'

No client_secret needed. The server verifies the code_verifier matches the code_challenge from Step 2.


Device Authorisation Flow (for CLI tools and IoT)

Building a CLI tool, a Raspberry Pi integration, or anything without a browser? Use the Device Authorisation Grant (RFC 8628).

Step 1: Request a device code

curl -X POST https://media.evercam.io/oauth/device/authorize \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "ev_YOUR_CLIENT_ID",
    "scope": "cameras:read camera:live_view"
  }'

Response:

{
  "device_code": "DEVICE_CODE",
  "user_code": "BDFG-HJKL",
  "verification_uri": "https://dash.evercam.io/oauth/device",
  "verification_uri_complete": "https://dash.evercam.io/oauth/device?user_code=BDFG-HJKL",
  "expires_in": 900,
  "interval": 5
}

Step 2: Show the user code

Display to the user:

To sign in, open https://dash.evercam.io/oauth/device
and enter code: BDFG-HJKL

Or show them a QR code pointing to verification_uri_complete.

Step 3: Poll for approval

curl -X POST https://media.evercam.io/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
    "device_code": "DEVICE_CODE",
    "client_id": "ev_YOUR_CLIENT_ID"
  }'

Possible responses while waiting:

  • "error": "authorization_pending" — user hasn't approved yet, keep polling

  • "error": "slow_down" — you're polling too fast, increase interval by 5 seconds

  • "error": "access_denied" — user denied the request

  • "error": "expired_token" — the device code expired (15 min TTL)

On success, you get the same access_token + refresh_token response as the other flows.

Respect the polling interval. Start at the interval value from Step 1 (default 5 seconds) and back off if you get slow_down.


Available scopes

Request only the scopes your app actually needs.

User

Scope

Description

user:info

Read user profile

profile:write

Modify user profile

Projects

Scope

Description

projects:read

List and view projects

Cameras

Scope

Description

cameras:read

List and view cameras

camera:live_view

Access live camera streams

camera:recordings

View camera recordings

camera:ptz

Control camera PTZ

Project features

Scope

Description

project:gate_report

Gate reporting

project:timeline

Project timeline

project:media_hub

Media management

project:bim

BIM integration

project:drone

Drone integration

project:360

360-degree view

project:planner

Project planning

project:smart_search

Smart search

project:ppe_monitoring

PPE monitoring

project:weather

Weather integration

project:video_wall

Video wall

project:automations

Automation rules

project:connectors

Third-party connectors

project:comments

Comments system

project:settings

Project settings

project:widgets

Widget management


Token lifecycle

Token

Lifetime

Notes

Authorization code

10 minutes

Single use

Access token

1 hour

Use Authorization: Bearer <token> header

Refresh token

30 days

Rotated on each use — save the new one

Device code

15 minutes

Single use, deleted after exchange

Revoking tokens

curl -X POST https://media.evercam.io/oauth/revoke \
  -H "Content-Type: application/json" \
  -d '{
    "token": "TOKEN_TO_REVOKE",
    "client_id": "ev_YOUR_CLIENT_ID",
    "client_secret": "evs_YOUR_SECRET"
  }'

Introspecting tokens

Check if a token is still valid and what scopes it has:

curl -X POST https://media.evercam.io/oauth/introspect \
  -H "Content-Type: application/json" \
  -d '{
    "token": "TOKEN_TO_CHECK",
    "client_id": "ev_YOUR_CLIENT_ID",
    "client_secret": "evs_YOUR_SECRET"
  }'

End-to-end example: React app with PKCE

Here's a minimal example for a Next.js or React app deployed on Vercel:

// lib/oauth.ts

const CLIENT_ID = process.env.NEXT_PUBLIC_EVERCAM_CLIENT_ID!;
const REDIRECT_URI = process.env.NEXT_PUBLIC_REDIRECT_URI!;
const AUTH_URL = 'https://media.evercam.io/oauth/authorize';
const TOKEN_URL = 'https://media.evercam.io/oauth/token';

function generateRandomString(length: number): string {
  const array = new Uint8Array(length);
  crypto.getRandomValues(array);
  return btoa(String.fromCharCode(...array))
    .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

async function sha256(plain: string): Promise<string> {
  const encoder = new TextEncoder();
  const data = encoder.encode(plain);
  const digest = await crypto.subtle.digest('SHA-256', data);
  return btoa(String.fromCharCode(...new Uint8Array(digest)))
    .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

export async function startLogin(scopes: string[] = ['cameras:read', 'camera:live_view']) {
  const state = generateRandomString(16);
  const codeVerifier = generateRandomString(32);
  const codeChallenge = await sha256(codeVerifier);

  sessionStorage.setItem('oauth_state', state);
  sessionStorage.setItem('pkce_verifier', codeVerifier);

  const params = new URLSearchParams({
    client_id: CLIENT_ID,
    redirect_uri: REDIRECT_URI,
    response_type: 'code',
    scope: scopes.join(' '),
    state,
    code_challenge: codeChallenge,
    code_challenge_method: 'S256',
  });

  window.location.href = `${AUTH_URL}?${params}`;
}

export async function handleCallback(searchParams: URLSearchParams) {
  const code = searchParams.get('code');
  const state = searchParams.get('state');
  const savedState = sessionStorage.getItem('oauth_state');
  const codeVerifier = sessionStorage.getItem('pkce_verifier');

  if (!code || state !== savedState || !codeVerifier) {
    throw new Error('Invalid OAuth callback');
  }

  const response = await fetch(TOKEN_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      grant_type: 'authorization_code',
      client_id: CLIENT_ID,
      code,
      redirect_uri: REDIRECT_URI,
      code_verifier: codeVerifier,
    }),
  });

  // Clean up
  sessionStorage.removeItem('oauth_state');
  sessionStorage.removeItem('pkce_verifier');

  if (!response.ok) {
    throw new Error('Token exchange failed');
  }

  return response.json(); // { access_token, refresh_token, expires_in }
}
// app/callback/page.tsx

'use client';
import { useEffect } from 'react';
import { useSearchParams, useRouter } from 'next/navigation';
import { handleCallback } from '@/lib/oauth';

export default function CallbackPage() {
  const searchParams = useSearchParams();
  const router = useRouter();

  useEffect(() => {
    handleCallback(searchParams)
      .then(tokens => {
        // Store tokens securely (httpOnly cookie via API route is ideal)
        localStorage.setItem('evercam_access_token', tokens.access_token);
        localStorage.setItem('evercam_refresh_token', tokens.refresh_token);
        router.push('/');
      })
      .catch(err => {
        console.error('OAuth error:', err);
        router.push('/login?error=auth_failed');
      });
  }, [searchParams, router]);

  return <p>Signing in...</p>;
}

Common mistakes

Mistake

What to do instead

Asking users to paste their JWT

Use any of the OAuth flows above

Requesting all scopes "just in case"

Request only what you need, users see the scope list on consent

Ignoring token expiry

Check expires_in and refresh proactively

Storing tokens in localStorage in production

Use httpOnly cookies via a BFF (backend-for-frontend) route

Not using PKCE for public clients

Always use PKCE with S256 for browser/mobile apps

Not validating state parameter

Always generate, store, and verify state to prevent CSRF


FAQs

Q: Can I use http://localhost as a redirect URI during development? Yes. Localhost redirect URIs are allowed over HTTP (per RFC 8252), and port matching is flexible — registering http://localhost:3000/callback will also work for other localhost ports.

Q: What happens if the user denies consent? They are redirected back to your redirect_uri with error=access_denied in the query string.

Q: Can I use OAuth for server-to-server (no user involved)? Not currently. All OAuth flows require a user to authorise. For server-to-server, use API keys (x-api-id + x-api-key headers).

Q: My app is a CLI tool, which flow should I use? Device Authorisation Flow. It shows the user a code to enter in their browser.

Q: How do I add more scopes later? Update your app’s scopes via PATCH /v2/developer/apps/:client_id. Existing tokens keep their original scopes — users will see the updated scope list on their next authorisation.


Need help?