Developer Docs Logo

OAuth Flow

This document explains how a agent installs your third-party app, and how your backend completes the OAuth handshake to get a workspace-scoped access token.


Overview

The CRM uses the standard OAuth 2.0 Authorization Code flow. Installation is started by the agent from inside the CRM Dashboard: they find your app, click Install, and approve the requested scopes on the CRM-hosted consent screen.

The CRM constructs the authorization URL and generates the state token internally, then redirects the browser straight to the consent page. Your third-party app backend is not involved until the callback.


Installation Flow

The handshake is implemented in three backend steps, walked through below.


Step 1: Handle the OAuth Callback

After the admin approves, the CRM redirects to your registered redirect_uri with query parameters.

redirect_uri requirements:

  • Must be HTTPS.
  • Must exactly match one of the Redirect URLs you registered in Developer Configuration (see App Setup). Any mismatch (including a trailing slash difference) causes the OAuth flow to fail.
  • localhost cannot be registered as a redirect URL (the dashboard requires a real domain with a TLD). For local development, use a tunneling tool such as ngrok or Cloudflare Tunnel to expose your local server under a public HTTPS URL, then register that URL.

Success Callback

GET /oauth2/crm/callback?code=XXXXXXXXX&scope=chat:read+chat:write+...&state=YYYY
ParameterDescription
codeSingle-use, short-lived authorization code. Exchange it immediately.
scopeThe scopes the admin actually approved (may differ from requested scopes)
stateA value generated by the CRM. Your app does not need to validate it.

Your handler simply pulls code out of the query string and moves on to Step 2.

Error Callback

If the admin denies or an error occurs:

GET /oauth2/crm/callback?error=access_denied&error_description=...&state=YYYY

Always handle the error case and show the admin a clear message.

Example in TypeScript

import type { Request, Response } from "express";

async function handleOAuthCallback(req: Request, res: Response) {
  const { code, error, error_description } = req.query as Record<string, string>;

  if (error) {
    return res.status(400).send(`Install failed: ${error_description ?? error}`);
  }

  if (!code) {
    return res.status(400).send("Missing authorization code");
  }

  const token = await exchangeToken(
    CRM_BASE_URL,
    CRM_CLIENT_ID,
    CRM_CLIENT_SECRET,
    code,
    CRM_REDIRECT_URI,
  );

  await saveInstallation(token);

  return res.redirect(APP_DASHBOARD_URL);
}

Step 2: Exchange the Code for an Access Token

Exchange the code you received in the callback for a workspace-scoped access token.

Endpoint

POST https://api.askyura.com/api/oauth2/token/exchange
Content-Type: application/json

Full OAuth endpoint specifications are available here.

Request Body

{
  "client_id": "<YOUR_CRM_CLIENT_ID>",
  "client_secret": "<YOUR_CRM_CLIENT_SECRET>",
  "code": "<RECEIVED_CODE>",
  "redirect_uri": "<YOUR_EXACT_REDIRECT_URI>"
}

The redirect_uri here must be HTTPS and must exactly match the one the CRM redirected the browser to, and must be registered in Developer Configuration.

Success Response (200 OK)

{
  "access_token": "eyJ...",
  "active": true,
  "workspace_id": "ws_abc123",
  "workspace_name": "Acme Support"
}

Error Response

{
  "message": "invalid code exchange payload",
  "slug": "invalid-exchange-token-payload"
}

Example in TypeScript

interface TokenExchangeResponse {
  access_token: string;
  active: boolean;
  workspace_id: string;
  workspace_name: string;
}

async function exchangeToken(
  crmBaseURL: string,
  clientID: string,
  clientSecret: string,
  code: string,
  redirectURI: string,
): Promise<TokenExchangeResponse> {
  const response = await fetch(`${crmBaseURL}/api/oauth2/token/exchange`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      client_id: clientID,
      client_secret: clientSecret,
      code,
      redirect_uri: redirectURI,
    }),
  });

  if (!response.ok) {
    const err = await response.json();
    throw new Error(`Token exchange failed: ${err.message}`);
  }

  return response.json();
}

Step 3: Store the Access Token

After a successful exchange, store the access token in your database, keyed by workspace_id. This is the credential you will use for every future CRM API call on behalf of that workspace.

Database Structure

Note: This schema is provided as an example/guideline. You may adapt it to your specific requirements, database system, and naming conventions.

CREATE TABLE workspace_installations (
    workspace_id    TEXT PRIMARY KEY,
    access_token    TEXT NOT NULL,
    workspace_name  TEXT,
    installed_at    TIMESTAMPTZ DEFAULT NOW(),
    updated_at      TIMESTAMPTZ DEFAULT NOW()
);

Once stored, redirect the admin to the App Dashboard URL configured in Developer Configuration. Installation is complete.


Post-Installation Concerns

The three steps above complete the OAuth handshake. The sections below cover ongoing implementation details that your backend also needs to handle.


Session Management (Admin Dashboard)

If your third-party app has a web-based admin dashboard (where admins configure platform accounts and channel mappings), you need session management to know which agent is logged in.

Why this is needed

The CRM access token you stored in Step 3 is for calling the CRM API on behalf of a workspace, it is not a session credential for your own dashboard. Without a separate session, your backend has no way to know who is making a request to your dashboard pages or API endpoints.

A session credential solves this: issued once after OAuth, carried automatically by the browser on every subsequent request, and verified by your backend middleware without touching the CRM.


Choosing an approach

ApproachStatelessStandardRevocableRecommended for
JWT in HttpOnly cookieYesYesNo*Most dashboards
Server-side session (Redis)NoYesYesWhen you need instant revocation

*A JWT cannot be invalidated before its expiry without maintaining a blocklist. For most 3PA dashboards this is acceptable, if you need to force-logout a workspace immediately (e.g. on deactivation), use a server-side session instead.

Avoid storing tokens in localStorage or sessionStorage. They are accessible to any JavaScript on the page, making them vulnerable to XSS. An HttpOnly cookie is not readable by JS and is the correct transport for session credentials.


JWT (JSON Web Token) is the standard for stateless dashboard sessions. The server signs the token with a secret key, no database lookup needed on each request. Libraries exist for every language and the format is auditable and widely understood.

Flow: Session creation (picks up after token exchange)

Flow: Every subsequent authenticated request

Flow: Expired or missing session


Alternative: Server-Side Session

If you need to invalidate sessions immediately (e.g. force-logout when a workspace is deactivated), store sessions in Redis instead.

Flow: Session creation and revocation

The tradeoff versus JWT: every request hits Redis, but you gain the ability to revoke any session instantly.


Handling Token Expiry and Re-installation

If the CRM returns 401 Unauthorized when you call any API with a stored token:

  1. Mark the workspace installation as disconnected in your DB.
  2. Email or notify the agent that they need to reconnect.
  3. Provide a button in your admin dashboard to re-trigger the OAuth flow.

There is currently no refresh token mechanism. The admin must re-authorize.

Note: In practice, token expiry is unlikely to occur at this time. The CRM currently issues access tokens with a very long expiration period, so the 401 flow described above may not be triggered in normal usage. This behavior may change in the future.


Handling Uninstallation

Currently, the CRM does not emit an app.uninstalled webhook event. This might be implemented in the future.

On this page