App Platform

Build embedded apps for WorkSkedge with JWT authentication, postMessage API, and secure iframe integration.

Introduction to the App Platform

The WorkSkedge App Platform enables developers to build embedded applications that run seamlessly within the WorkSkedge interface. Your apps are loaded in secure iframes with full access to user context, authentication tokens, and real-time communication capabilities.

Authentication & Security: All embedded apps use JWT token authentication. See the Authentication tab for comprehensive security guidance, token handling, and role-based access control.

Secure Authentication

JWT tokens with user context, company data, and role-based permissions.

Real-time Communication

Bidirectional postMessage API for seamless app-to-platform integration.

Built-in Testing

Dev Launcher with token inspection and custom URL testing.

Architecture Overview

Your app runs inside an iframe within WorkSkedge. The platform handles authentication, passes JWT tokens with user context, and provides a postMessage API for real-time communication.

┌─────────────────────────────────────────────────────────┐
│                    WorkSkedge Platform                  │
│                                                         │
│  1. User opens app → Platform generates JWT token      │
│  2. Token includes: userId, companyId, roles, etc.     │
│  3. Token passed via URL + postMessage                 │
│                                                         │
│  ┌───────────────────────────────────────────────────┐ │
│  │              Your Embedded App (iframe)           │ │
│  │                                                   │ │
│  │  1. Receive & decode JWT token                   │ │
│  │  2. Verify token on your server                  │ │
│  │  3. Access user data from token claims           │ │
│  │  4. Use postMessage for communication            │ │
│  └───────────────────────────────────────────────────┘ │
│                                                         │
│  ← postMessage: context updates, navigation, etc.      │
│  → postMessage: ready, notify, refresh, etc.           │
└─────────────────────────────────────────────────────────┘

Integration Comparison

Choose the right integration method for your use case:

Feature Embedded Apps REST API Webhooks
User Interface Full UI inside WorkSkedge External UI No UI
User Context Automatic via JWT Manual API calls Event payloads
Authentication JWT tokens (1hr) API keys Webhook signatures
Real-time Updates postMessage API Polling required Push notifications
Best For Custom dashboards, forms, visualizations Integrations, data sync, automation Event-driven workflows, notifications

Quick Start Workflow

Follow these steps to build your first embedded app:

1

Create Your App

Build a web application that accepts JWT tokens via URL parameters and postMessage.

2

Handle Authentication

Decode the JWT token client-side to access user context, then verify it on your server.

3

Implement postMessage

Use the postMessage API to communicate with WorkSkedge for token updates and navigation.

4

Test with Dev Launcher

Use the built-in Dev Launcher to test your app with real tokens before going live.

5

Deploy & Install

Deploy your app to production and install it for your WorkSkedge company.

Requirements Checklist

Before building your embedded app, ensure you have:

A web server to host your application (can be any hosting platform)
HTTPS endpoint (required for secure iframe embedding)
JWT token verification logic (client-side and server-side)
postMessage event listeners for WorkSkedge communication
Error handling for expired or invalid tokens
Testing with Dev Launcher before production deployment

Next Steps

Session Token Verification Guide

When your app is embedded in WorkSkedge, it receives a signed JWT session token containing the authenticated user's identity, roles, and company context. You can verify these tokens using the Session Signing Secret provided in the app.installed webhook.

Token Expiration: JWT tokens expire after 1 hour. Your app will automatically receive new tokens via postMessage before expiration.

Token Verification Methods

Recommended: Self-Verification

Verify tokens yourself using the sessionSigningSecret from your installation. This is:

  • Faster - No network call required
  • More reliable - No external dependencies
  • Offline capable - Works without internet

Alternative: API Verification

Call the WorkSkedge API to verify tokens. Use this if you prefer not to implement JWT verification.

Self-Verification (Recommended)

Node.js

Self-Verification with Session Signing Secret JavaScript
const crypto = require('crypto');

function verifySessionToken(token, sessionSigningSecret) {
  const parts = token.split('.');
  if (parts.length !== 3) {
    return { valid: false, error: 'Invalid token format' };
  }

  const [headerB64, payloadB64, signatureB64] = parts;

  // Verify signature
  const data = `${headerB64}.${payloadB64}`;
  const expectedSignature = crypto
    .createHmac('sha256', sessionSigningSecret)
    .update(data)
    .digest('base64url');

  if (signatureB64 !== expectedSignature) {
    return { valid: false, error: 'Invalid signature' };
  }

  // Decode payload
  const payload = JSON.parse(Buffer.from(payloadB64, 'base64url').toString());

  // Check expiration
  if (payload.exp * 1000 < Date.now()) {
    return { valid: false, error: 'Token expired' };
  }

  return { valid: true, payload };
}

// Usage
app.post('/api/protected', (req, res) => {
  const { token, installId } = req.body;

  // Get the session signing secret for this installation
  const installation = await db.installations.findByInstallId(installId);

  const result = verifySessionToken(token, installation.sessionSigningSecret);

  if (!result.valid) {
    return res.status(401).json({ error: result.error });
  }

  // Token is valid, user is authenticated
  const { employeeId, employeeName, roles, companyId } = result.payload;
  // ... handle request
});

Python

Self-Verification with Session Signing Secret Python
import hmac
import hashlib
import base64
import json
import time

def verify_session_token(token: str, session_signing_secret: str) -> dict:
    parts = token.split('.')
    if len(parts) != 3:
        return {'valid': False, 'error': 'Invalid token format'}

    header_b64, payload_b64, signature_b64 = parts

    # Verify signature
    data = f"{header_b64}.{payload_b64}"
    expected_sig = base64.urlsafe_b64encode(
        hmac.new(
            session_signing_secret.encode(),
            data.encode(),
            hashlib.sha256
        ).digest()
    ).rstrip(b'=').decode()

    if signature_b64 != expected_sig:
        return {'valid': False, 'error': 'Invalid signature'}

    # Decode payload
    # Add padding if needed
    padding = 4 - len(payload_b64) % 4
    if padding != 4:
        payload_b64 += '=' * padding

    payload = json.loads(base64.urlsafe_b64decode(payload_b64))

    # Check expiration
    if payload['exp'] < time.time():
        return {'valid': False, 'error': 'Token expired'}

    return {'valid': True, 'payload': payload}

# Usage
@app.route('/api/protected', methods=['POST'])
def protected_endpoint():
    data = request.json
    token = data.get('token')
    install_id = data.get('installId')

    installation = Installation.query.filter_by(install_id=install_id).first()
    result = verify_session_token(token, installation.session_signing_secret)

    if not result['valid']:
        return jsonify({'error': result['error']}), 401

    payload = result['payload']
    # Token is valid, proceed with request

Go

Self-Verification with Session Signing Secret Go
package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/base64"
    "encoding/json"
    "errors"
    "strings"
    "time"
)

type TokenPayload struct {
    Sub          string   `json:"sub"`
    CompanyID    string   `json:"companyId"`
    InstallID    string   `json:"installId"`
    AppID        string   `json:"appId"`
    EmployeeID   *string  `json:"employeeId"`
    EmployeeName string   `json:"employeeName"`
    Email        string   `json:"email"`
    Roles        []string `json:"roles"`
    Iat          int64    `json:"iat"`
    Exp          int64    `json:"exp"`
}

func VerifySessionToken(token, sessionSigningSecret string) (*TokenPayload, error) {
    parts := strings.Split(token, ".")
    if len(parts) != 3 {
        return nil, errors.New("invalid token format")
    }

    headerB64, payloadB64, signatureB64 := parts[0], parts[1], parts[2]

    // Verify signature
    data := headerB64 + "." + payloadB64
    h := hmac.New(sha256.New, []byte(sessionSigningSecret))
    h.Write([]byte(data))
    expectedSig := base64.RawURLEncoding.EncodeToString(h.Sum(nil))

    if signatureB64 != expectedSig {
        return nil, errors.New("invalid signature")
    }

    // Decode payload
    payloadBytes, err := base64.RawURLEncoding.DecodeString(payloadB64)
    if err != nil {
        return nil, errors.New("invalid payload encoding")
    }

    var payload TokenPayload
    if err := json.Unmarshal(payloadBytes, &payload); err != nil {
        return nil, errors.New("invalid payload format")
    }

    // Check expiration
    if payload.Exp < time.Now().Unix() {
        return nil, errors.New("token expired")
    }

    return &payload, nil
}

API Verification (Alternative)

If you prefer not to implement JWT verification yourself, call the WorkSkedge API:

API Verification JavaScript
async function verifyTokenViaAPI(token, installId) {
  const response = await fetch(
    'https://bktpmukmqrxpuiskvvaq.supabase.co/functions/v1/apps-verify-session-token',
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ token, installId })
    }
  );

  return response.json();
}

Response Format

Successful Verification JSON
{
  "valid": true,
  "payload": {
    "userId": "uuid",
    "companyId": "uuid",
    "installId": "uuid",
    "appId": "uuid",
    "employeeId": "uuid",
    "employeeName": "John Doe",
    "email": "john@example.com",
    "roles": ["administrator", "scheduler"],
    "issuedAt": 1234567890,
    "expiresAt": 1234571490
  }
}
Failed Verification JSON
{
  "valid": false,
  "error": "Token expired"
}

Token Structure

The JWT session token contains the following claims:

JWT Token Payload JSON
{
  // Standard JWT Claims
  iss: "workskedge",        // Issuer
  sub: "uuid",              // User ID
  iat: 1234567890,          // Issued at (Unix timestamp)
  exp: 1234571490,          // Expires at (Unix timestamp, 1 hour from issue)

  // WorkSkedge Claims
  companyId: "uuid",        // Company ID
  installId: "uuid",        // App installation ID
  appId: "uuid",            // App catalog ID
  employeeId: "uuid | null",// Employee ID (if user is linked to an employee)
  employeeName: "string",   // Display name
  email: "string",          // User email
  roles: ["string"],        // Array of user roles
  nonce: "uuid"             // Unique identifier to prevent replay attacks
}

Receiving Tokens in Embedded Apps

Initial Load (URL Parameter)

When your app loads in an iframe, extract the token from URL parameters:

Extract Token from URL JavaScript
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get('token');
const installId = urlParams.get('installId');

if (token) {
  // Verify and use the token
  const result = await verifyToken(token, installId);
  if (result.valid) {
    console.log('User:', result.payload.employeeName);
    console.log('Roles:', result.payload.roles);
  }
}

Token Refresh (postMessage)

Listen for token refresh messages from WorkSkedge:

Listen for Token Updates JavaScript
window.addEventListener('message', (event) => {
  // Validate origin
  if (!event.origin.includes('workskedge')) return;

  const message = event.data;
  if (message.type === 'workskedge:context') {
    const { token, tokenExpiry } = message.payload;
    // Update your stored token
    updateStoredToken(token);
    console.log('Token refreshed, expires:', new Date(tokenExpiry));
  }
});

Requesting Fresh Context

Your app can request updated context at any time:

Request Context Update JavaScript
window.parent.postMessage({
  type: 'workskedge:request_context',
  requestId: crypto.randomUUID()
}, '*');

Available Roles

Users can have one or more of these roles:

Role Description
administrator Full company access, settings, user management
manager Employee management, all projects, reports
scheduler Schedule management, work orders, assignments
project_owner Own projects, assigned employees, reports
site_lead Own team, daily reports, schedule visibility
site_worker Own schedule, timesheets, basic access

Role-Based Access Control Example

Role Checking Functions JavaScript
function canEditWorkOrders(roles) {
  return roles.some(r =>
    ['administrator', 'manager', 'scheduler', 'project_owner'].includes(r)
  );
}

function canViewAllEmployees(roles) {
  return roles.some(r =>
    ['administrator', 'manager', 'scheduler'].includes(r)
  );
}

Security Best Practices

Always Verify Tokens

Always verify tokens for any sensitive operation, never trust client-side data alone.

Check Token Expiration

Check token expiration before using cached tokens. Tokens expire after 1 hour.

Validate the installId

Ensure the installId in the token matches your expected installation.

Use the Nonce

Use the nonce claim for additional replay attack prevention in critical operations.

Never Trust Client-Side Data

Always verify tokens on your server before performing sensitive operations.

Store Session Signing Secret Securely

Never expose the Session Signing Secret to clients. Store it server-side only.

Testing During Development

Using the Dev Launcher

On the Private Apps page, each app has a Launch dropdown:

Option Description
Launch Dev Mode Opens your development URL with a valid JWT token
Launch Production Opens your production URL with a valid JWT token
View Token Info Shows the decoded JWT payload for debugging

Development Workflow

  1. Create a Private App with Supports Embedding enabled
  2. Set your Development URL (e.g., http://localhost:3000)
  3. Use "Launch Dev Mode" to test with real authentication
  4. Inspect tokens with "View Token Info" to verify correct data

Troubleshooting

Token is null on load

  • Ensure your app waits for the WorkSkedge embed to generate the token
  • Check browser console for authentication errors

Token expired immediately

  • Server time may be out of sync
  • Check that exp claim is being read as Unix timestamp (seconds, not milliseconds)

Verification fails with "Invalid signature"

  • Ensure you're using the correct sessionSigningSecret for this installation
  • The app may have been uninstalled and reinstalled (new secret generated)
  • Check that you're using the correct base64url encoding

postMessage not received

  • Check that you're listening for messages on the correct origin
  • Verify the iframe has proper sandbox permissions

Related Documentation

App Installation Guide

When a company installs your app, WorkSkedge notifies you via webhooks with everything you need to operate: API keys for making API calls, session signing secrets for verifying user tokens, and installation details. This guide explains the complete installation lifecycle.

One Webhook, Everything You Need: The app.installed webhook contains your API Key, Session Signing Secret, and company details in a single payload.

Quick Start

When a company installs your app, you receive everything needed in a single app.installed webhook:

What You Get Where It Comes From What It's For
API Key credentials.apiKey Authenticate API calls to WorkSkedge
Session Signing Secret credentials.sessionSigningSecret Self-verify user JWT tokens in embedded apps
App Secret Shown once at app creation Verify HMAC-SHA256 signatures on ALL webhooks

Webhook Flow

Here's how the installation process works:

┌─────────────────────────────────────────────────────────────────────┐
│                        APP INSTALLATION FLOW                         │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  1. User clicks "Install App" in WorkSkedge                         │
│                          ↓                                           │
│  2. WorkSkedge creates:                                              │
│     - company_app_installs record                                    │
│     - API Key for this installation                                  │
│     - Session Signing Secret for JWT verification                    │
│                          ↓                                           │
│  3. WorkSkedge sends POST to {developer_website}/webhooks/app       │
│     with app.installed event payload (includes credentials)          │
│                          ↓                                           │
│  4. Your app receives and processes the webhook:                    │
│     - Verify signature using App Secret                              │
│     - Store API Key for making API calls                             │
│     - Store Session Signing Secret for verifying user tokens         │
│                          ↓                                           │
│  5. Your app returns HTTP 200 OK                                     │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

Webhook Events

WorkSkedge sends these lifecycle events to your webhook endpoint:

Event Description
app.installed Sent when a company installs your app (includes credentials)
app.uninstalled Sent when a company uninstalls your app
app.updated Sent when app settings are modified

app.installed Payload

This is the most important webhook you'll receive. It contains everything needed to operate your app:

app.installed Event Payload JSON
{
  "event": "app.installed",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "data": {
    "installId": "550e8400-e29b-41d4-a716-446655440000",
    "appId": "660e8400-e29b-41d4-a716-446655440001",
    "appName": "Time Tracker Pro",
    "company": {
      "id": "770e8400-e29b-41d4-a716-446655440002",
      "name": "Acme Construction",
      "industry": "construction",
      "employeeCount": 50,
      "timezone": "America/New_York",
      "modulesEnabled": {
        "core": true,
        "timesheets": true,
        "purchaseOrders": true
      }
    },
    "credentials": {
      "apiKey": "ws_key_abc123...",
      "sessionSigningSecret": "ws_sess_def456..."
    },
    "grantedScopes": [
      "employees:read",
      "timesheets:read",
      "timesheets:write"
    ],
    "subscribedEvents": [
      "timesheet.created",
      "timesheet.submitted",
      "employee.created"
    ],
    "allowedRoles": ["administrator", "manager"],
    "installedBy": {
      "userId": "880e8400-e29b-41d4-a716-446655440003",
      "email": "admin@acme.com",
      "name": "John Admin"
    },
    "installedAt": "2024-01-15T10:30:00.000Z"
  }
}

app.uninstalled Payload

app.uninstalled Event Payload JSON
{
  "event": "app.uninstalled",
  "timestamp": "2024-02-15T14:00:00.000Z",
  "data": {
    "installId": "550e8400-e29b-41d4-a716-446655440000",
    "appId": "660e8400-e29b-41d4-a716-446655440001",
    "appName": "Time Tracker Pro",
    "company": {
      "id": "770e8400-e29b-41d4-a716-446655440002",
      "name": "Acme Construction"
    }
  }
}

Webhook Headers

WorkSkedge includes these headers with all webhooks:

Header Description
X-WorkSkedge-Signature HMAC-SHA256 signature for verification
X-WorkSkedge-Event Event type (e.g., app.installed)
X-WorkSkedge-Delivery-Id Unique delivery attempt ID
X-WorkSkedge-Install-Id Installation ID

Signature Verification

Verify webhook authenticity using your App Secret (provided when you created your app):

Node.js Example

Webhook Signature Verification JavaScript
const crypto = require('crypto');

function verifyWebhook(req, appSecret) {
  const signature = req.headers['x-workskedge-signature'];
  const rawBody = req.rawBody; // Must be raw string, not parsed JSON

  const expectedSignature = crypto
    .createHmac('sha256', appSecret)
    .update(rawBody)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

// Express.js example
app.post('/webhooks/app', express.raw({ type: 'application/json' }), (req, res) => {
  if (!verifyWebhook(req, process.env.APP_SECRET)) {
    return res.status(401).send('Invalid signature');
  }

  const event = JSON.parse(req.body);

  switch (event.event) {
    case 'app.installed':
      handleInstallation(event.data);
      break;
    case 'app.uninstalled':
      handleUninstallation(event.data);
      break;
    case 'app.updated':
      handleUpdate(event.data);
      break;
  }

  res.status(200).send('OK');
});

async function handleInstallation(data) {
  // Store credentials for this installation
  await db.installations.create({
    installId: data.installId,
    companyId: data.company.id,
    companyName: data.company.name,
    apiKey: data.credentials.apiKey,
    sessionSigningSecret: data.credentials.sessionSigningSecret,
  });

  console.log(`App installed by ${data.company.name}`);
}

Python Example

Webhook Signature Verification Python
import hmac
import hashlib
from flask import Flask, request

app = Flask(__name__)

def verify_webhook(request, app_secret):
    signature = request.headers.get('X-WorkSkedge-Signature')
    raw_body = request.data

    expected = hmac.new(
        app_secret.encode(),
        raw_body,
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(signature, expected)

@app.route('/webhooks/app', methods=['POST'])
def handle_app_webhook():
    if not verify_webhook(request, os.environ['APP_SECRET']):
        return 'Invalid signature', 401

    event = request.json

    if event['event'] == 'app.installed':
        handle_installation(event['data'])
    elif event['event'] == 'app.uninstalled':
        handle_uninstallation(event['data'])

    return 'OK', 200

def handle_installation(data):
    # Store credentials for this installation
    Installation.create(
        install_id=data['installId'],
        company_id=data['company']['id'],
        company_name=data['company']['name'],
        api_key=data['credentials']['apiKey'],
        session_signing_secret=data['credentials']['sessionSigningSecret']
    )

Using the Credentials

Making API Calls

Use the apiKey from the installation to authenticate API calls:

API Call with API Key JavaScript
const response = await fetch('https://bktpmukmqrxpuiskvvaq.supabase.co/functions/v1/api-employees', {
  headers: {
    'X-API-Key': installation.apiKey,
    'Content-Type': 'application/json'
  }
});

Verifying Session Tokens

Use the sessionSigningSecret to verify JWT tokens in embedded apps. See the Authentication tab for details.

Business Webhooks

In addition to lifecycle webhooks (app.installed, app.uninstalled, app.updated), your app can subscribe to business event webhooks. All webhooks use the same App Secret for signature verification.

Available Business Events

Event Description
work_order.created A work order was created
work_order.updated A work order was modified
work_order.status_changed A work order status changed
timesheet.submitted A timesheet was submitted for approval
timesheet.locked A timesheet was locked
timesheet.processed A timesheet was marked as processed
employee.created An employee was added
employee.updated An employee was modified

Business Webhook Payload Example

work_order.created Event JSON
{
  "event": "work_order.created",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "data": {
    "workOrderId": "wo-123",
    "workOrderNumber": "WO-2024-001",
    "projectId": "proj-456",
    "projectName": "Office Renovation",
    "status": "draft",
    "createdBy": "user-789"
  }
}

Verifying Business Webhooks

Use the same App Secret and verification logic for all webhooks:

Unified Webhook Handler JavaScript
// Same verification for ALL webhooks (lifecycle and business)
app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
  if (!verifyWebhook(req, process.env.APP_SECRET)) {
    return res.status(401).send('Invalid signature');
  }

  const event = JSON.parse(req.body);

  // Handle both lifecycle and business events
  switch (event.event) {
    case 'app.installed':
      handleInstallation(event.data);
      break;
    case 'work_order.created':
      handleWorkOrderCreated(event.data);
      break;
    case 'timesheet.submitted':
      handleTimesheetSubmitted(event.data);
      break;
    // ... other events
  }

  res.status(200).send('OK');
});

Best Practices

Always Verify Signatures

Never trust unverified webhook payloads. Always verify the HMAC signature.

Store Credentials Securely

API Key and Session Signing Secret are sensitive. Store them encrypted in your database.

Handle Idempotently

Webhooks may be retried; use deliveryId for deduplication.

Return 200 Quickly

Process webhooks asynchronously to avoid timeouts. Return 200 immediately.

Log Delivery IDs

Store delivery IDs for debugging and support inquiries.

Use Raw Request Body

Verify signatures against the raw body, not parsed JSON.

Troubleshooting

Webhook Not Received

  • Verify developer_website is configured correctly in your app settings
  • Ensure your /webhooks/app endpoint is publicly accessible
  • Check for firewall rules blocking WorkSkedge IPs
  • Review webhook delivery logs in WorkSkedge admin

Invalid Signature

  • Ensure you're using the raw request body (not parsed JSON)
  • Verify you're using the correct App Secret (provided when you created the app)
  • Check for whitespace or encoding issues in the payload
  • If you regenerated your secret, make sure your app is using the new one

Missing Credentials

  • credentials is only included in app.installed events
  • If you missed the initial webhook, you may need to reinstall the app

Related Documentation

App Credentials Reference

This is the single source of truth for all credentials used in the WorkSkedge App Platform. Understanding when, where, and how to use each credential type is essential for building secure integrations.

Overview

The WorkSkedge App Platform uses three types of credentials:

Credential Scope When Received Purpose
App Secret Per App At app creation Verify webhook signatures
API Key Per Installation app.installed webhook Authenticate API calls
Session Signing Secret Per Installation app.installed webhook Verify user JWT tokens

1. App Secret

What It Is

A cryptographic secret used to verify the authenticity of webhooks sent by WorkSkedge.

When You Get It

  • Shown once when you create your app in WorkSkedge
  • Copy and store it immediately - it cannot be retrieved later
  • Can be regenerated from app settings (invalidates old secret)

What It's For

  • Verify HMAC-SHA256 signatures on all webhooks from WorkSkedge
  • Includes lifecycle webhooks (app.installed, app.uninstalled, app.updated)
  • Includes business webhooks (work_order.created, timesheet.submitted, etc.)

How to Use It

Verify Webhook Signature JavaScript
const crypto = require('crypto');

function verifyWebhook(rawBody, signature, appSecret) {
  const expected = crypto
    .createHmac('sha256', appSecret)
    .update(rawBody)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

// In your webhook handler
app.post('/webhooks', (req, res) => {
  const signature = req.headers['x-workskedge-signature'];

  if (!verifyWebhook(req.rawBody, signature, process.env.APP_SECRET)) {
    return res.status(401).send('Invalid signature');
  }

  // Process webhook...
});

Security Notes

  • Store in environment variables, never in code
  • Same secret for all companies that install your app
  • Rotate periodically for enhanced security

2. API Key

What It Is

A unique key that authenticates your app when making API calls to WorkSkedge on behalf of a specific company.

When You Get It

  • Received in the app.installed webhook payload
  • Located at: data.credentials.apiKey
  • Format: ws_key_...

What It's For

  • Authenticate REST API calls to WorkSkedge
  • Access company data within the granted scopes
  • Each installation gets its own unique API key

How to Use It

Store and Use API Key JavaScript
// Store the API key when you receive the app.installed webhook
async function handleInstallation(data) {
  await db.installations.create({
    installId: data.installId,
    companyId: data.company.id,
    apiKey: data.credentials.apiKey,
    // ... other fields
  });
}

// Use the API key for subsequent API calls
async function getEmployees(installId) {
  const installation = await db.installations.findByInstallId(installId);

  const response = await fetch(
    'https://bktpmukmqrxpuiskvvaq.supabase.co/functions/v1/api-employees',
    {
      headers: {
        'X-API-Key': installation.apiKey,
        'Content-Type': 'application/json'
      }
    }
  );

  return response.json();
}

Security Notes

  • Store per-installation (each company gets a unique key)
  • Never expose to client-side code
  • Scoped to the permissions granted during installation

3. Session Signing Secret

What It Is

A cryptographic secret used to verify JWT session tokens for users accessing your embedded app.

When You Get It

  • Received in the app.installed webhook payload
  • Located at: data.credentials.sessionSigningSecret
  • Format: ws_sess_...

What It's For

  • Self-verify JWT tokens passed to your embedded app
  • Authenticate users without making API calls
  • Faster and more reliable than API-based verification

How to Use It

Verify Session Token JavaScript
const crypto = require('crypto');

function verifySessionToken(token, sessionSigningSecret) {
  const [headerB64, payloadB64, signatureB64] = token.split('.');

  // Verify signature
  const data = `${headerB64}.${payloadB64}`;
  const expected = crypto
    .createHmac('sha256', sessionSigningSecret)
    .update(data)
    .digest('base64url');

  if (signatureB64 !== expected) {
    return { valid: false, error: 'Invalid signature' };
  }

  // Decode and check expiration
  const payload = JSON.parse(Buffer.from(payloadB64, 'base64url').toString());

  if (payload.exp * 1000 < Date.now()) {
    return { valid: false, error: 'Token expired' };
  }

  return { valid: true, payload };
}

// In your embedded app's backend
app.post('/api/verify-user', (req, res) => {
  const { token, installId } = req.body;
  const installation = await db.installations.findByInstallId(installId);

  const result = verifySessionToken(token, installation.sessionSigningSecret);

  if (!result.valid) {
    return res.status(401).json({ error: result.error });
  }

  // User is authenticated
  res.json({ user: result.payload });
});

Security Notes

  • Store per-installation (each company gets a unique secret)
  • Never expose to client-side code
  • Tokens expire after 1 hour; WorkSkedge auto-refreshes them

Credential Lifecycle

On App Creation

┌─────────────────────────────────────┐
│         APP CREATION                │
├─────────────────────────────────────┤
│                                     │
│  1. Developer creates app           │
│           ↓                         │
│  2. WorkSkedge generates App Secret │
│           ↓                         │
│  3. App Secret shown ONCE           │
│           ↓                         │
│  4. Developer stores App Secret     │
│                                     │
└─────────────────────────────────────┘

On App Installation

┌─────────────────────────────────────┐
│         APP INSTALLATION            │
├─────────────────────────────────────┤
│                                     │
│  1. Company clicks "Install"        │
│           ↓                         │
│  2. WorkSkedge generates:           │
│     - API Key (per installation)    │
│     - Session Signing Secret        │
│           ↓                         │
│  3. app.installed webhook sent      │
│     - Signed with App Secret        │
│     - Contains credentials object   │
│           ↓                         │
│  4. Developer stores credentials    │
│                                     │
└─────────────────────────────────────┘

Quick Reference Table

Question App Secret API Key Session Signing Secret
When do I get it? App creation app.installed webhook app.installed webhook
Where in payload? N/A (shown in UI) credentials.apiKey credentials.sessionSigningSecret
One per...? App Installation Installation
Can I regenerate? Yes (in settings) No (reinstall app) No (reinstall app)
Use for what? Verify webhooks Authenticate API calls Verify user tokens
In which header? Verify X-WorkSkedge-Signature Send X-API-Key N/A (JWT signature)

Troubleshooting

"Invalid signature" on webhooks

  • Ensure you're using the App Secret (not API Key or Session Signing Secret)
  • Use the raw request body, not parsed JSON
  • Check if you regenerated the secret and need to update your app

"Unauthorized" on API calls

  • Ensure you're using the API Key in the X-API-Key header
  • Verify the key is for the correct installation
  • Check that the app is still installed and active

"Invalid token" on session verification

  • Ensure you're using the Session Signing Secret for that installation
  • Check token expiration (tokens last 1 hour)
  • Verify base64url encoding in your verification code

Credential Rotation

Rotating the App Secret

When you regenerate your App Secret in app settings:

  • Immediate Effect: Old secret becomes invalid immediately
  • Impact: All webhook signature verification will fail until you update your app
  • No Impact On: API Keys and Session Signing Secrets remain valid

Graceful Rotation Procedure

Support Both Old and New Secrets JavaScript
// Support both old and new secrets during transition
function verifyWebhookWithRotation(rawBody, signature, secrets) {
  for (const secret of secrets) {
    const expected = crypto
      .createHmac('sha256', secret)
      .update(rawBody)
      .digest('hex');

    if (crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
      return true;
    }
  }
  return false;
}

// Use: verifyWebhookWithRotation(body, sig, [NEW_SECRET, OLD_SECRET])

Recommended Steps

  1. Deploy code that accepts both old and new secrets
  2. Regenerate the App Secret in WorkSkedge
  3. Update your environment with the new secret
  4. Remove old secret from your code after confirming it works

Rotating API Keys and Session Signing Secrets

These credentials cannot be regenerated - they are tied to the installation. To get new credentials:

  1. Have the company uninstall your app
  2. Have them reinstall it
  3. You'll receive new credentials in the app.installed webhook
Impact of Reinstallation: New API Key issued (old one invalidated), new Session Signing Secret issued, all active user sessions will be invalidated, and any stored API key on your side must be updated.

Related Documentation