Integration Guides

End-to-end tutorials for building real-world integrations with WorkSkedge.

Getting Started with WorkSkedge API

This guide will walk you through making your first API request to WorkSkedge, from generating your API key to handling responses.

Step 1: Generate an API Key

Before you can make API requests, you need to generate an API key from your WorkSkedge account.

1

Log into WorkSkedge

Navigate to app.workskedge.com and sign in to your account.

2

Access Settings

Click your profile icon in the top right, then select Settings from the dropdown menu.

3

Navigate to API Keys

In the settings sidebar, click on API Keys to view your API key management page.

4

Generate New Key

Click Generate New Key, give it a descriptive name (e.g., "Development Integration"), select the appropriate scopes, and save your key securely.

Important: Your API key will only be shown once. Store it securely in a password manager or environment variable. Never commit API keys to version control or share them publicly.

Step 2: Make Your First Request

Let's fetch a list of projects from your WorkSkedge account. We'll show examples in JavaScript, Python, and cURL.

// Fetch list of projects
const API_KEY = 'your_api_key_here';
const BASE_URL = 'https://app.workskedge.com/api/v1';

async function getProjects() {
  try {
    const response = await fetch(`${BASE_URL}/projects`, {
      method: 'GET',
      headers: {
        'Authorization': `Bearer ${API_KEY}`,
        'Content-Type': 'application/json'
      }
    });

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    const data = await response.json();
    console.log('Projects:', data);
    return data;
  } catch (error) {
    console.error('Error fetching projects:', error);
  }
}

getProjects();
import requests

API_KEY = 'your_api_key_here'
BASE_URL = 'https://app.workskedge.com/api/v1'

def get_projects():
    """Fetch list of projects"""
    headers = {
        'Authorization': f'Bearer {API_KEY}',
        'Content-Type': 'application/json'
    }

    try:
        response = requests.get(f'{BASE_URL}/projects', headers=headers)
        response.raise_for_status()

        projects = response.json()
        print('Projects:', projects)
        return projects
    except requests.exceptions.RequestException as error:
        print(f'Error fetching projects: {error}')
        return None

get_projects()
curl -X GET "https://app.workskedge.com/api/v1/projects" \
  -H "Authorization: Bearer your_api_key_here" \
  -H "Content-Type: application/json"

Step 3: Understanding the Response

A successful API response will return a JSON object with your data and metadata for pagination.

{
  "data": [
    {
      "id": "proj_1234567890",
      "name": "Downtown Office Renovation",
      "status": "active",
      "customer_id": "cust_abc123",
      "start_date": "2025-01-15",
      "end_date": "2025-06-30",
      "created_at": "2025-01-01T10:00:00Z",
      "updated_at": "2025-01-10T14:30:00Z"
    },
    {
      "id": "proj_0987654321",
      "name": "Industrial Warehouse Build",
      "status": "in_progress",
      "customer_id": "cust_xyz789",
      "start_date": "2025-02-01",
      "end_date": "2025-12-31",
      "created_at": "2025-01-05T09:00:00Z",
      "updated_at": "2025-01-12T11:15:00Z"
    }
  ],
  "pagination": {
    "total": 45,
    "limit": 20,
    "offset": 0,
    "has_more": true
  }
}

The pagination object helps you navigate through large datasets. Use the limit and offset query parameters to fetch subsequent pages.

Step 4: Create a New Resource

Now let's create a new work order. This demonstrates a POST request with a request body.

async function createWorkOrder() {
  const workOrderData = {
    title: "Install HVAC System",
    description: "Install new HVAC system in building A",
    project_id: "proj_1234567890",
    assigned_to: ["emp_abc123", "emp_xyz789"],
    scheduled_start: "2025-01-20T08:00:00Z",
    scheduled_end: "2025-01-20T17:00:00Z",
    priority: "high"
  };

  try {
    const response = await fetch(`${BASE_URL}/work-orders`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${API_KEY}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(workOrderData)
    });

    if (!response.ok) {
      const error = await response.json();
      throw new Error(error.message);
    }

    const newWorkOrder = await response.json();
    console.log('Created work order:', newWorkOrder);
    return newWorkOrder;
  } catch (error) {
    console.error('Error creating work order:', error);
  }
}

createWorkOrder();
def create_work_order():
    """Create a new work order"""
    work_order_data = {
        'title': 'Install HVAC System',
        'description': 'Install new HVAC system in building A',
        'project_id': 'proj_1234567890',
        'assigned_to': ['emp_abc123', 'emp_xyz789'],
        'scheduled_start': '2025-01-20T08:00:00Z',
        'scheduled_end': '2025-01-20T17:00:00Z',
        'priority': 'high'
    }

    headers = {
        'Authorization': f'Bearer {API_KEY}',
        'Content-Type': 'application/json'
    }

    try:
        response = requests.post(
            f'{BASE_URL}/work-orders',
            headers=headers,
            json=work_order_data
        )
        response.raise_for_status()

        new_work_order = response.json()
        print('Created work order:', new_work_order)
        return new_work_order
    except requests.exceptions.RequestException as error:
        print(f'Error creating work order: {error}')
        return None

create_work_order()

Step 5: Handle Errors Gracefully

Always implement proper error handling to deal with network issues, rate limits, and validation errors.

async function makeApiRequest(endpoint, options = {}) {
  const maxRetries = 3;
  let retries = 0;

  while (retries < maxRetries) {
    try {
      const response = await fetch(`${BASE_URL}${endpoint}`, {
        ...options,
        headers: {
          'Authorization': `Bearer ${API_KEY}`,
          'Content-Type': 'application/json',
          ...options.headers
        }
      });

      // Handle rate limiting
      if (response.status === 429) {
        const retryAfter = response.headers.get('Retry-After') || 60;
        console.log(`Rate limited. Retrying after ${retryAfter} seconds...`);
        await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
        retries++;
        continue;
      }

      // Handle other errors
      if (!response.ok) {
        const error = await response.json();
        throw new Error(`API Error: ${error.message || response.statusText}`);
      }

      return await response.json();
    } catch (error) {
      if (retries >= maxRetries - 1) {
        throw error;
      }
      retries++;
      await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, retries)));
    }
  }
}

// Usage
try {
  const projects = await makeApiRequest('/projects');
  console.log('Projects:', projects);
} catch (error) {
  console.error('Failed after retries:', error);
}

Setting Up Webhooks

Webhooks allow you to receive real-time notifications when events occur in WorkSkedge. This guide covers everything from creating your first webhook endpoint to handling events at scale.

Understanding Webhooks

Webhooks are HTTP callbacks that WorkSkedge sends to your server when specific events occur. Instead of constantly polling our API for changes, webhooks push updates to you instantly.

Polling (Without Webhooks)

  • Requires frequent API calls to check for changes
  • Delays between when events occur and when you discover them
  • Consumes API rate limit quota unnecessarily
  • More complex to implement correctly

Webhooks (Recommended)

  • Instant notifications when events occur
  • Real-time updates with no delays
  • No API rate limit impact for receiving events
  • Simpler and more efficient implementation

Step 1: Create a Webhook Endpoint

First, you need to create an endpoint on your server that can receive webhook events from WorkSkedge.

const express = require('express');
const crypto = require('crypto');
const app = express();

app.use(express.json());

// Webhook secret from WorkSkedge settings
const WEBHOOK_SECRET = process.env.WORKSKEDGE_WEBHOOK_SECRET;

// Verify webhook signature
function verifySignature(payload, signature) {
  const hmac = crypto.createHmac('sha256', WEBHOOK_SECRET);
  const digest = hmac.update(payload).digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(digest)
  );
}

app.post('/webhooks/workskedge', (req, res) => {
  const signature = req.headers['x-workskedge-signature'];
  const payload = JSON.stringify(req.body);

  // Verify the request came from WorkSkedge
  if (!verifySignature(payload, signature)) {
    console.error('Invalid webhook signature');
    return res.status(401).send('Invalid signature');
  }

  const event = req.body;
  console.log('Received webhook event:', event.event_type);

  // Handle different event types
  switch (event.event_type) {
    case 'work_order.created':
      handleWorkOrderCreated(event.data);
      break;
    case 'work_order.updated':
      handleWorkOrderUpdated(event.data);
      break;
    case 'project.created':
      handleProjectCreated(event.data);
      break;
    default:
      console.log('Unhandled event type:', event.event_type);
  }

  // Respond quickly to acknowledge receipt
  res.status(200).json({ received: true });
});

function handleWorkOrderCreated(data) {
  console.log('New work order created:', data.id);
  // Your business logic here
}

function handleWorkOrderUpdated(data) {
  console.log('Work order updated:', data.id);
  // Your business logic here
}

function handleProjectCreated(data) {
  console.log('New project created:', data.id);
  // Your business logic here
}

app.listen(3000, () => {
  console.log('Webhook server listening on port 3000');
});
from flask import Flask, request, jsonify
import hmac
import hashlib
import os

app = Flask(__name__)

WEBHOOK_SECRET = os.environ.get('WORKSKEDGE_WEBHOOK_SECRET')

def verify_signature(payload, signature):
    """Verify webhook signature"""
    mac = hmac.new(
        WEBHOOK_SECRET.encode(),
        msg=payload.encode(),
        digestmod=hashlib.sha256
    )
    return hmac.compare_digest(mac.hexdigest(), signature)

@app.route('/webhooks/workskedge', methods=['POST'])
def handle_webhook():
    signature = request.headers.get('X-Workskedge-Signature')
    payload = request.get_data(as_text=True)

    # Verify the request came from WorkSkedge
    if not verify_signature(payload, signature):
        print('Invalid webhook signature')
        return jsonify({'error': 'Invalid signature'}), 401

    event = request.json
    print(f"Received webhook event: {event['event_type']}")

    # Handle different event types
    event_type = event['event_type']
    if event_type == 'work_order.created':
        handle_work_order_created(event['data'])
    elif event_type == 'work_order.updated':
        handle_work_order_updated(event['data'])
    elif event_type == 'project.created':
        handle_project_created(event['data'])
    else:
        print(f"Unhandled event type: {event_type}")

    # Respond quickly to acknowledge receipt
    return jsonify({'received': True}), 200

def handle_work_order_created(data):
    print(f"New work order created: {data['id']}")
    # Your business logic here

def handle_work_order_updated(data):
    print(f"Work order updated: {data['id']}")
    # Your business logic here

def handle_project_created(data):
    print(f"New project created: {data['id']}")
    # Your business logic here

if __name__ == '__main__':
    app.run(port=3000)
<?php
$webhookSecret = $_ENV['WORKSKEDGE_WEBHOOK_SECRET'];

// Get webhook payload
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_WORKSKEDGE_SIGNATURE'] ?? '';

// Verify webhook signature
function verifySignature($payload, $signature, $secret) {
    $computedSignature = hash_hmac('sha256', $payload, $secret);
    return hash_equals($computedSignature, $signature);
}

if (!verifySignature($payload, $signature, $webhookSecret)) {
    http_response_code(401);
    echo json_encode(['error' => 'Invalid signature']);
    exit;
}

$event = json_decode($payload, true);
error_log('Received webhook event: ' . $event['event_type']);

// Handle different event types
switch ($event['event_type']) {
    case 'work_order.created':
        handleWorkOrderCreated($event['data']);
        break;
    case 'work_order.updated':
        handleWorkOrderUpdated($event['data']);
        break;
    case 'project.created':
        handleProjectCreated($event['data']);
        break;
    default:
        error_log('Unhandled event type: ' . $event['event_type']);
}

// Respond quickly to acknowledge receipt
http_response_code(200);
echo json_encode(['received' => true]);

function handleWorkOrderCreated($data) {
    error_log('New work order created: ' . $data['id']);
    // Your business logic here
}

function handleWorkOrderUpdated($data) {
    error_log('Work order updated: ' . $data['id']);
    // Your business logic here
}

function handleProjectCreated($data) {
    error_log('New project created: ' . $data['id']);
    // Your business logic here
}
?>

Step 2: Configure Your Webhook in WorkSkedge

Once your endpoint is ready and accessible from the internet, configure it in WorkSkedge.

1

Navigate to Webhooks Settings

Log into WorkSkedge, go to Settings > Webhooks.

2

Add New Webhook

Click Add Webhook and enter your endpoint URL (e.g., https://yourdomain.com/webhooks/workskedge).

3

Select Events

Choose which events you want to receive. Start with a few common events like work_order.created and work_order.updated.

4

Save Webhook Secret

WorkSkedge will generate a webhook secret. Save this securely as an environment variable.

5

Test Your Webhook

Use the Send Test Event button to verify your endpoint is working correctly.

Step 3: Handle Webhook Events

Each webhook event has a consistent structure with an event type and data payload.

{
  "event_id": "evt_1234567890abcdef",
  "event_type": "work_order.created",
  "timestamp": "2025-01-15T10:30:00Z",
  "data": {
    "id": "wo_abc123xyz789",
    "title": "Install HVAC System",
    "description": "Install new HVAC system in building A",
    "status": "scheduled",
    "priority": "high",
    "project_id": "proj_1234567890",
    "assigned_to": ["emp_abc123", "emp_xyz789"],
    "scheduled_start": "2025-01-20T08:00:00Z",
    "scheduled_end": "2025-01-20T17:00:00Z",
    "created_at": "2025-01-15T10:30:00Z",
    "updated_at": "2025-01-15T10:30:00Z"
  }
}

See the Webhooks Documentation for a complete list of all 31 available webhook events and their payload structures.

Best Practices

1. Respond Quickly

Always return a 200 OK response within 5 seconds. If processing takes longer, acknowledge receipt immediately and process the event asynchronously using a queue.

app.post('/webhooks/workskedge', async (req, res) => {
  // Respond immediately
  res.status(200).json({ received: true });

  // Process asynchronously
  await queue.add('webhook-processing', {
    event: req.body
  });
});

2. Implement Idempotency

WorkSkedge may send the same webhook multiple times for reliability. Use the event_id to prevent duplicate processing.

const processedEvents = new Set();

function handleWebhookEvent(event) {
  // Check if we've already processed this event
  if (processedEvents.has(event.event_id)) {
    console.log('Duplicate event, skipping:', event.event_id);
    return;
  }

  // Process the event
  processEvent(event);

  // Mark as processed
  processedEvents.add(event.event_id);
}

3. Verify Signatures

Always verify the X-Workskedge-Signature header to ensure the webhook came from WorkSkedge and hasn't been tampered with.

const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, secret) {
  const hmac = crypto.createHmac('sha256', secret);
  const computedSignature = hmac.update(payload).digest('hex');

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

4. Handle Failures Gracefully

If your endpoint fails, WorkSkedge will retry with exponential backoff. Implement proper error handling and logging.

app.post('/webhooks/workskedge', async (req, res) => {
  try {
    // Verify signature
    if (!verifySignature(req.body, req.headers['x-workskedge-signature'])) {
      return res.status(401).send('Invalid signature');
    }

    // Respond immediately
    res.status(200).json({ received: true });

    // Process event with error handling
    try {
      await processWebhookEvent(req.body);
    } catch (error) {
      console.error('Error processing webhook:', error);
      // Log to error tracking service
      errorTracker.captureException(error, {
        extra: { event: req.body }
      });
    }
  } catch (error) {
    console.error('Critical webhook error:', error);
    res.status(500).send('Internal server error');
  }
});

Testing Webhooks Locally

During development, you need to expose your local server to the internet for WorkSkedge to reach it. We recommend using ngrok.

1

Install ngrok

# Download from https://ngrok.com or use package manager
brew install ngrok  # macOS
# or
npm install -g ngrok  # npm
2

Start Your Local Server

node server.js  # Start your webhook endpoint on port 3000
3

Create ngrok Tunnel

ngrok http 3000

ngrok will give you a public URL like https://abc123.ngrok.io

4

Configure in WorkSkedge

Use the ngrok URL as your webhook endpoint: https://abc123.ngrok.io/webhooks/workskedge

Remember: ngrok URLs change each time you restart. For production, use a permanent domain with HTTPS.

Common Webhook Patterns

Pattern 1: Sync to External System

When a work order is created in WorkSkedge, automatically create a corresponding ticket in your help desk system.

async function handleWorkOrderCreated(workOrder) {
  try {
    // Create ticket in external system
    const ticket = await helpDeskAPI.createTicket({
      title: workOrder.title,
      description: workOrder.description,
      priority: workOrder.priority,
      external_id: workOrder.id
    });

    console.log(`Created help desk ticket ${ticket.id} for work order ${workOrder.id}`);
  } catch (error) {
    console.error('Failed to sync work order:', error);
  }
}

Pattern 2: Send Notifications

Notify team members via email or Slack when they're assigned to a work order.

async function handleWorkOrderCreated(workOrder) {
  // Fetch employee details
  const employees = await Promise.all(
    workOrder.assigned_to.map(id => getEmployee(id))
  );

  // Send notifications
  for (const employee of employees) {
    await sendEmail({
      to: employee.email,
      subject: `New Assignment: ${workOrder.title}`,
      body: `You've been assigned to: ${workOrder.title}\n\n` +
            `Scheduled: ${workOrder.scheduled_start}\n` +
            `Priority: ${workOrder.priority}`
    });

    await sendSlackMessage({
      channel: employee.slack_id,
      text: `🔨 New work order assigned: ${workOrder.title}`
    });
  }
}

Pattern 3: Update Analytics

Track work order metrics in your analytics dashboard.

async function handleWorkOrderStatusChanged(workOrder) {
  await analytics.track({
    event: 'work_order_status_changed',
    properties: {
      work_order_id: workOrder.id,
      old_status: workOrder.previous_status,
      new_status: workOrder.status,
      project_id: workOrder.project_id,
      duration_hours: calculateDuration(workOrder)
    }
  });

  // Update real-time dashboard
  await dashboard.updateMetric('active_work_orders', {
    increment: workOrder.status === 'in_progress' ? 1 : 0,
    decrement: workOrder.status === 'completed' ? 1 : 0
  });
}

REST API Integration Patterns

Learn common patterns and best practices for building robust integrations with the WorkSkedge REST API.

Pattern 1: Pagination - Fetching All Resources

When fetching large datasets, use pagination to retrieve all records efficiently without hitting memory limits.

const API_KEY = process.env.WORKSKEDGE_API_KEY;
const BASE_URL = 'https://app.workskedge.com/api/v1';

async function fetchAllProjects() {
  const allProjects = [];
  let offset = 0;
  const limit = 100; // Max allowed per request
  let hasMore = true;

  while (hasMore) {
    const response = await fetch(
      `${BASE_URL}/projects?limit=${limit}&offset=${offset}`,
      {
        headers: {
          'Authorization': `Bearer ${API_KEY}`,
          'Content-Type': 'application/json'
        }
      }
    );

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    const result = await response.json();

    allProjects.push(...result.data);
    hasMore = result.pagination.has_more;
    offset += limit;

    console.log(`Fetched ${allProjects.length} of ${result.pagination.total} projects`);
  }

  return allProjects;
}

// Usage
const allProjects = await fetchAllProjects();
console.log(`Total projects fetched: ${allProjects.length}`);

Tip: Add a small delay between requests to avoid rate limiting: await sleep(100)

Pattern 2: Bulk Operations with Batch Processing

When creating or updating multiple resources, process them in batches to optimize performance and handle rate limits.

async function batchCreateEmployees(employees) {
  const BATCH_SIZE = 10;
  const DELAY_MS = 1000; // Wait 1 second between batches
  const results = [];
  const errors = [];

  for (let i = 0; i < employees.length; i += BATCH_SIZE) {
    const batch = employees.slice(i, i + BATCH_SIZE);

    console.log(`Processing batch ${Math.floor(i / BATCH_SIZE) + 1} of ${Math.ceil(employees.length / BATCH_SIZE)}`);

    const batchPromises = batch.map(async (employee) => {
      try {
        const response = await fetch(`${BASE_URL}/employees`, {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${API_KEY}`,
            'Content-Type': 'application/json'
          },
          body: JSON.stringify(employee)
        });

        if (!response.ok) {
          const error = await response.json();
          throw new Error(error.message);
        }

        const result = await response.json();
        return { success: true, data: result };
      } catch (error) {
        return {
          success: false,
          employee: employee,
          error: error.message
        };
      }
    });

    const batchResults = await Promise.all(batchPromises);

    batchResults.forEach(result => {
      if (result.success) {
        results.push(result.data);
      } else {
        errors.push(result);
      }
    });

    // Wait before processing next batch (rate limiting)
    if (i + BATCH_SIZE < employees.length) {
      await new Promise(resolve => setTimeout(resolve, DELAY_MS));
    }
  }

  return {
    successful: results.length,
    failed: errors.length,
    results,
    errors
  };
}

// Usage
const employeesToCreate = [
  { first_name: 'John', last_name: 'Doe', email: 'john@example.com', role: 'technician' },
  { first_name: 'Jane', last_name: 'Smith', email: 'jane@example.com', role: 'supervisor' },
  // ... more employees
];

const result = await batchCreateEmployees(employeesToCreate);
console.log(`Created ${result.successful} employees, ${result.failed} failed`);

Pattern 3: Optimistic Updates with Error Recovery

Update your UI immediately for better user experience, then sync with the API and handle any conflicts.

class WorkOrderManager {
  constructor(apiKey) {
    this.apiKey = apiKey;
    this.pendingUpdates = new Map();
  }

  async updateWorkOrder(workOrderId, updates) {
    // Store original state for rollback
    const originalState = await this.getWorkOrder(workOrderId);

    // Apply optimistic update to local state/UI
    this.applyOptimisticUpdate(workOrderId, updates);

    try {
      // Send update to API
      const response = await fetch(
        `${BASE_URL}/work-orders/${workOrderId}`,
        {
          method: 'PUT',
          headers: {
            'Authorization': `Bearer ${this.apiKey}`,
            'Content-Type': 'application/json'
          },
          body: JSON.stringify(updates)
        }
      );

      if (!response.ok) {
        throw new Error(`Failed to update: ${response.status}`);
      }

      const updatedWorkOrder = await response.json();

      // Confirm the update
      this.confirmUpdate(workOrderId, updatedWorkOrder);
      return updatedWorkOrder;

    } catch (error) {
      console.error('Update failed, rolling back:', error);

      // Rollback to original state
      this.rollbackUpdate(workOrderId, originalState);

      // Show error to user
      this.showError(`Failed to update work order: ${error.message}`);

      throw error;
    }
  }

  applyOptimisticUpdate(id, updates) {
    // Update local state immediately
    console.log(`Optimistically updating work order ${id}`);
    this.pendingUpdates.set(id, updates);
    // Update UI here
  }

  confirmUpdate(id, serverState) {
    console.log(`Confirmed update for work order ${id}`);
    this.pendingUpdates.delete(id);
    // Update UI with server state
  }

  rollbackUpdate(id, originalState) {
    console.log(`Rolling back work order ${id} to original state`);
    this.pendingUpdates.delete(id);
    // Restore original state in UI
  }

  async getWorkOrder(id) {
    const response = await fetch(`${BASE_URL}/work-orders/${id}`, {
      headers: {
        'Authorization': `Bearer ${this.apiKey}`,
        'Content-Type': 'application/json'
      }
    });
    return await response.json();
  }

  showError(message) {
    // Display error to user
    console.error(message);
  }
}

// Usage
const manager = new WorkOrderManager(API_KEY);

try {
  await manager.updateWorkOrder('wo_123', {
    status: 'in_progress'
  });
} catch (error) {
  // Error already handled by manager
}

Pattern 4: Caching with Smart Invalidation

Implement caching to reduce API calls, but invalidate the cache intelligently when data changes.

class CachedAPIClient {
  constructor(apiKey, cacheDuration = 300000) { // 5 minutes default
    this.apiKey = apiKey;
    this.cacheDuration = cacheDuration;
    this.cache = new Map();
  }

  getCacheKey(endpoint, params = {}) {
    return `${endpoint}?${new URLSearchParams(params).toString()}`;
  }

  isCacheValid(cacheEntry) {
    if (!cacheEntry) return false;
    return Date.now() - cacheEntry.timestamp < this.cacheDuration;
  }

  async get(endpoint, params = {}) {
    const cacheKey = this.getCacheKey(endpoint, params);
    const cached = this.cache.get(cacheKey);

    // Return cached data if valid
    if (this.isCacheValid(cached)) {
      console.log(`Cache hit for ${cacheKey}`);
      return cached.data;
    }

    // Fetch fresh data
    console.log(`Cache miss for ${cacheKey}, fetching...`);
    const queryString = new URLSearchParams(params).toString();
    const url = `${BASE_URL}${endpoint}${queryString ? '?' + queryString : ''}`;

    const response = await fetch(url, {
      headers: {
        'Authorization': `Bearer ${this.apiKey}`,
        'Content-Type': 'application/json'
      }
    });

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    const data = await response.json();

    // Cache the response
    this.cache.set(cacheKey, {
      data,
      timestamp: Date.now()
    });

    return data;
  }

  async post(endpoint, body) {
    const response = await fetch(`${BASE_URL}${endpoint}`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${this.apiKey}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(body)
    });

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    // Invalidate related cache entries
    this.invalidateCache(endpoint);

    return await response.json();
  }

  async put(endpoint, body) {
    const response = await fetch(`${BASE_URL}${endpoint}`, {
      method: 'PUT',
      headers: {
        'Authorization': `Bearer ${this.apiKey}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(body)
    });

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    // Invalidate related cache entries
    this.invalidateCache(endpoint);

    return await response.json();
  }

  invalidateCache(endpoint) {
    // Remove all cache entries for this endpoint
    const baseEndpoint = endpoint.split('/').slice(0, -1).join('/');
    for (const key of this.cache.keys()) {
      if (key.startsWith(baseEndpoint)) {
        this.cache.delete(key);
        console.log(`Invalidated cache for ${key}`);
      }
    }
  }

  clearCache() {
    this.cache.clear();
    console.log('Cache cleared');
  }
}

// Usage
const client = new CachedAPIClient(API_KEY);

// First call - fetches from API
const projects1 = await client.get('/projects');

// Second call within 5 minutes - returns cached data
const projects2 = await client.get('/projects');

// Create new project - invalidates cache
await client.post('/projects', {
  name: 'New Project',
  customer_id: 'cust_123'
});

// Next call fetches fresh data
const projects3 = await client.get('/projects');

Pattern 5: Bidirectional Sync (Preventing Echo Storms)

When syncing data between WorkSkedge and another system, prevent infinite update loops with proper tracking.

class BidirectionalSync {
  constructor(workskedgeAPI, externalAPI) {
    this.workskedgeAPI = workskedgeAPI;
    this.externalAPI = externalAPI;
    this.syncMap = new Map(); // Track synced records
    this.processingUpdates = new Set(); // Prevent echo storms
  }

  // Store mapping between WorkSkedge ID and external system ID
  storeSyncMapping(workskedgeId, externalId, lastSyncTime = null) {
    this.syncMap.set(workskedgeId, {
      externalId,
      lastSync: lastSyncTime || Date.now()
    });
  }

  getSyncMapping(workskedgeId) {
    return this.syncMap.get(workskedgeId);
  }

  // Handle webhook from WorkSkedge
  async handleWorkskedgeWebhook(event) {
    const workOrderId = event.data.id;

    // Check if we're currently processing an update for this record
    if (this.processingUpdates.has(workOrderId)) {
      console.log(`Ignoring echo for ${workOrderId}`);
      return;
    }

    // Mark as processing
    this.processingUpdates.add(workOrderId);

    try {
      const syncInfo = this.getSyncMapping(workOrderId);

      if (!syncInfo) {
        // New work order - create in external system
        const externalTicket = await this.externalAPI.createTicket({
          title: event.data.title,
          description: event.data.description,
          status: event.data.status
        });

        this.storeSyncMapping(workOrderId, externalTicket.id);
      } else {
        // Update existing ticket in external system
        await this.externalAPI.updateTicket(syncInfo.externalId, {
          title: event.data.title,
          description: event.data.description,
          status: event.data.status
        });

        this.storeSyncMapping(workOrderId, syncInfo.externalId);
      }
    } finally {
      // Remove from processing set after a delay
      setTimeout(() => {
        this.processingUpdates.delete(workOrderId);
      }, 5000); // 5 second cooldown
    }
  }

  // Handle webhook from external system
  async handleExternalWebhook(externalEvent) {
    const externalTicketId = externalEvent.ticket.id;

    // Find corresponding WorkSkedge work order
    let workOrderId = null;
    for (const [wsId, syncInfo] of this.syncMap.entries()) {
      if (syncInfo.externalId === externalTicketId) {
        workOrderId = wsId;
        break;
      }
    }

    if (!workOrderId) {
      console.log('No mapping found for external ticket');
      return;
    }

    // Check if we're currently processing an update
    if (this.processingUpdates.has(workOrderId)) {
      console.log(`Ignoring echo for ${workOrderId}`);
      return;
    }

    this.processingUpdates.add(workOrderId);

    try {
      // Update work order in WorkSkedge
      await this.workskedgeAPI.updateWorkOrder(workOrderId, {
        title: externalEvent.ticket.title,
        description: externalEvent.ticket.description,
        status: this.mapExternalStatus(externalEvent.ticket.status)
      });

      this.storeSyncMapping(workOrderId, externalTicketId);
    } finally {
      setTimeout(() => {
        this.processingUpdates.delete(workOrderId);
      }, 5000);
    }
  }

  mapExternalStatus(externalStatus) {
    const statusMap = {
      'open': 'scheduled',
      'in_progress': 'in_progress',
      'resolved': 'completed',
      'closed': 'completed'
    };
    return statusMap[externalStatus] || 'scheduled';
  }
}

// Usage
const sync = new BidirectionalSync(workskedgeClient, externalClient);

// Handle WorkSkedge webhook
app.post('/webhooks/workskedge', async (req, res) => {
  res.status(200).send('OK');
  await sync.handleWorkskedgeWebhook(req.body);
});

// Handle external system webhook
app.post('/webhooks/external', async (req, res) => {
  res.status(200).send('OK');
  await sync.handleExternalWebhook(req.body);
});

Critical: Always track which updates you initiated to prevent infinite update loops between systems. Use a combination of processing flags and cooldown periods for reliability.

Pattern 6: Search and Filter

Use query parameters to filter and search for specific resources efficiently.

async function searchWorkOrders(filters) {
  const params = new URLSearchParams();

  // Add filters
  if (filters.status) params.append('status', filters.status);
  if (filters.priority) params.append('priority', filters.priority);
  if (filters.project_id) params.append('project_id', filters.project_id);
  if (filters.assigned_to) params.append('assigned_to', filters.assigned_to);
  if (filters.scheduled_after) params.append('scheduled_after', filters.scheduled_after);
  if (filters.scheduled_before) params.append('scheduled_before', filters.scheduled_before);

  // Pagination
  params.append('limit', filters.limit || 20);
  params.append('offset', filters.offset || 0);

  const response = await fetch(
    `${BASE_URL}/work-orders?${params.toString()}`,
    {
      headers: {
        'Authorization': `Bearer ${API_KEY}`,
        'Content-Type': 'application/json'
      }
    }
  );

  if (!response.ok) {
    throw new Error(`Search failed: ${response.status}`);
  }

  return await response.json();
}

// Usage examples

// Find all high priority work orders
const highPriorityWorkOrders = await searchWorkOrders({
  priority: 'high',
  status: 'scheduled'
});

// Find work orders for a specific project
const projectWorkOrders = await searchWorkOrders({
  project_id: 'proj_123',
  scheduled_after: '2025-01-01'
});

// Find work orders assigned to a specific employee
const employeeWorkOrders = await searchWorkOrders({
  assigned_to: 'emp_abc123',
  status: 'in_progress'
});

Best Practices Summary

Use Pagination

Always paginate when fetching large datasets to avoid memory issues and timeouts

Batch Operations

Process bulk operations in batches with delays to respect rate limits

Implement Caching

Cache frequently accessed data but invalidate appropriately when data changes

Handle Errors

Implement proper error handling with retries and exponential backoff

Prevent Echo Storms

Track updates you initiate to prevent infinite loops in bidirectional syncs

Monitor Rate Limits

Check rate limit headers and implement backoff strategies

Error Handling and Retry Strategies

Build robust integrations by implementing proper error handling, retry logic, and graceful degradation strategies.

Understanding API Error Responses

WorkSkedge API returns standardized error responses with HTTP status codes and detailed error messages.

4xx Client Errors

Errors caused by invalid requests. Do not retry without fixing the request.

  • 400 Bad Request: Invalid parameters or malformed JSON
  • 401 Unauthorized: Invalid or missing API key
  • 403 Forbidden: API key lacks required permissions
  • 404 Not Found: Resource doesn't exist
  • 422 Unprocessable Entity: Validation errors
  • 429 Too Many Requests: Rate limit exceeded

5xx Server Errors

Temporary server issues. Safe to retry with exponential backoff.

  • 500 Internal Server Error: Unexpected server error
  • 502 Bad Gateway: Gateway or proxy error
  • 503 Service Unavailable: Temporary maintenance
  • 504 Gateway Timeout: Request timeout

Error Response Format

{
  "error": {
    "type": "validation_error",
    "message": "Invalid project_id",
    "code": "invalid_parameter",
    "param": "project_id",
    "details": {
      "project_id": "Project 'proj_invalid' does not exist"
    }
  }
}

Exponential Backoff Strategy

Implement exponential backoff to handle temporary failures gracefully without overwhelming the API.

class RetryStrategy {
  constructor(options = {}) {
    this.maxRetries = options.maxRetries || 3;
    this.baseDelay = options.baseDelay || 1000; // 1 second
    this.maxDelay = options.maxDelay || 32000;  // 32 seconds
    this.retryableStatuses = new Set([429, 500, 502, 503, 504]);
  }

  shouldRetry(error, attemptNumber) {
    // Don't retry if max attempts reached
    if (attemptNumber >= this.maxRetries) {
      return false;
    }

    // Retry on network errors
    if (error.name === 'TypeError' || error.name === 'NetworkError') {
      return true;
    }

    // Retry on specific HTTP status codes
    if (error.status && this.retryableStatuses.has(error.status)) {
      return true;
    }

    return false;
  }

  getDelay(attemptNumber) {
    // Calculate exponential backoff: baseDelay * 2^attempt
    const exponentialDelay = this.baseDelay * Math.pow(2, attemptNumber);

    // Add jitter (random 0-25% of delay) to prevent thundering herd
    const jitter = Math.random() * exponentialDelay * 0.25;

    // Cap at maxDelay
    return Math.min(exponentialDelay + jitter, this.maxDelay);
  }

  async executeWithRetry(fn) {
    let lastError;

    for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
      try {
        return await fn();
      } catch (error) {
        lastError = error;

        if (!this.shouldRetry(error, attempt)) {
          throw error;
        }

        const delay = this.getDelay(attempt);
        console.log(`Attempt ${attempt + 1} failed. Retrying in ${delay}ms...`);

        await new Promise(resolve => setTimeout(resolve, delay));
      }
    }

    throw lastError;
  }
}

// Usage
const retry = new RetryStrategy({
  maxRetries: 3,
  baseDelay: 1000
});

const projects = await retry.executeWithRetry(async () => {
  const response = await fetch('https://app.workskedge.com/api/v1/projects', {
    headers: {
      'Authorization': `Bearer ${API_KEY}`,
      'Content-Type': 'application/json'
    }
  });

  if (!response.ok) {
    const error = new Error(`HTTP ${response.status}`);
    error.status = response.status;
    throw error;
  }

  return await response.json();
});

Rate Limit Handling

Handle rate limits by respecting the Retry-After header and implementing a request queue.

class RateLimitHandler {
  constructor(apiKey) {
    this.apiKey = apiKey;
    this.queue = [];
    this.processing = false;
    this.rateLimit = {
      remaining: 60,
      reset: Date.now() + 60000
    };
  }

  updateRateLimitInfo(headers) {
    this.rateLimit = {
      remaining: parseInt(headers.get('X-RateLimit-Remaining') || '60'),
      reset: parseInt(headers.get('X-RateLimit-Reset') || Date.now() + 60000)
    };
  }

  async waitForRateLimit() {
    if (this.rateLimit.remaining > 0) {
      return;
    }

    const waitTime = Math.max(0, this.rateLimit.reset - Date.now());
    console.log(`Rate limit reached. Waiting ${waitTime}ms...`);
    await new Promise(resolve => setTimeout(resolve, waitTime));
  }

  async request(endpoint, options = {}) {
    return new Promise((resolve, reject) => {
      this.queue.push({ endpoint, options, resolve, reject });
      this.processQueue();
    });
  }

  async processQueue() {
    if (this.processing || this.queue.length === 0) {
      return;
    }

    this.processing = true;

    while (this.queue.length > 0) {
      await this.waitForRateLimit();

      const { endpoint, options, resolve, reject } = this.queue.shift();

      try {
        const response = await fetch(endpoint, {
          ...options,
          headers: {
            'Authorization': `Bearer ${this.apiKey}`,
            'Content-Type': 'application/json',
            ...options.headers
          }
        });

        // Update rate limit info from response headers
        this.updateRateLimitInfo(response.headers);

        if (response.status === 429) {
          const retryAfter = response.headers.get('Retry-After');
          const waitTime = retryAfter ? parseInt(retryAfter) * 1000 : 60000;

          console.log(`Rate limited. Waiting ${waitTime}ms before retry...`);
          await new Promise(r => setTimeout(r, waitTime));

          // Re-queue the request
          this.queue.unshift({ endpoint, options, resolve, reject });
          continue;
        }

        if (!response.ok) {
          const error = await response.json();
          reject(new Error(error.message || `HTTP ${response.status}`));
          continue;
        }

        const data = await response.json();
        resolve(data);
      } catch (error) {
        reject(error);
      }
    }

    this.processing = false;
  }
}

// Usage
const handler = new RateLimitHandler(API_KEY);

// All requests automatically queued and rate limited
const project1 = await handler.request('https://app.workskedge.com/api/v1/projects/proj_1');
const project2 = await handler.request('https://app.workskedge.com/api/v1/projects/proj_2');
const project3 = await handler.request('https://app.workskedge.com/api/v1/projects/proj_3');

Comprehensive Error Handler

Combine retry logic, rate limit handling, and error classification into a single robust client.

class RobustAPIClient {
  constructor(apiKey, options = {}) {
    this.apiKey = apiKey;
    this.baseUrl = options.baseUrl || 'https://app.workskedge.com/api/v1';
    this.retryStrategy = new RetryStrategy(options.retry);
    this.rateLimitHandler = new RateLimitHandler(apiKey);
    this.errorCallbacks = [];
  }

  onError(callback) {
    this.errorCallbacks.push(callback);
  }

  notifyError(error, context) {
    this.errorCallbacks.forEach(callback => {
      try {
        callback(error, context);
      } catch (err) {
        console.error('Error in error callback:', err);
      }
    });
  }

  async request(endpoint, options = {}) {
    const url = `${this.baseUrl}${endpoint}`;
    const context = {
      endpoint,
      method: options.method || 'GET',
      timestamp: new Date().toISOString()
    };

    try {
      return await this.retryStrategy.executeWithRetry(async () => {
        return await this.rateLimitHandler.request(url, options);
      });
    } catch (error) {
      const enhancedError = this.enhanceError(error, context);
      this.notifyError(enhancedError, context);
      throw enhancedError;
    }
  }

  enhanceError(error, context) {
    const enhanced = {
      message: error.message,
      context,
      timestamp: new Date().toISOString(),
      retryable: false,
      userMessage: 'An error occurred'
    };

    if (error.status) {
      enhanced.status = error.status;

      switch (error.status) {
        case 400:
          enhanced.userMessage = 'Invalid request. Please check your input.';
          break;
        case 401:
          enhanced.userMessage = 'Authentication failed. Please check your API key.';
          break;
        case 403:
          enhanced.userMessage = 'Permission denied. Your API key lacks required permissions.';
          break;
        case 404:
          enhanced.userMessage = 'Resource not found.';
          break;
        case 422:
          enhanced.userMessage = 'Validation error. Please check your input.';
          break;
        case 429:
          enhanced.userMessage = 'Rate limit exceeded. Please try again later.';
          enhanced.retryable = true;
          break;
        case 500:
        case 502:
        case 503:
        case 504:
          enhanced.userMessage = 'Server error. Please try again later.';
          enhanced.retryable = true;
          break;
        default:
          enhanced.userMessage = `An error occurred (${error.status})`;
      }
    }

    return enhanced;
  }

  async get(endpoint, params = {}) {
    const queryString = new URLSearchParams(params).toString();
    const url = queryString ? `${endpoint}?${queryString}` : endpoint;
    return this.request(url, { method: 'GET' });
  }

  async post(endpoint, data) {
    return this.request(endpoint, {
      method: 'POST',
      body: JSON.stringify(data)
    });
  }

  async put(endpoint, data) {
    return this.request(endpoint, {
      method: 'PUT',
      body: JSON.stringify(data)
    });
  }

  async delete(endpoint) {
    return this.request(endpoint, { method: 'DELETE' });
  }
}

// Usage
const client = new RobustAPIClient(API_KEY);

// Register error handler
client.onError((error, context) => {
  console.error('API Error:', {
    message: error.message,
    status: error.status,
    endpoint: context.endpoint,
    retryable: error.retryable
  });

  // Send to error tracking service
  // errorTracker.captureException(error, { extra: context });

  // Show user-friendly message
  if (error.userMessage) {
    // displayNotification(error.userMessage);
  }
});

// Make requests with automatic retry and rate limiting
try {
  const projects = await client.get('/projects');
  console.log('Projects:', projects);
} catch (error) {
  console.error('Failed to fetch projects:', error.userMessage);
}

Circuit Breaker Pattern

Prevent cascading failures by implementing a circuit breaker that stops making requests after repeated failures.

class CircuitBreaker {
  constructor(options = {}) {
    this.failureThreshold = options.failureThreshold || 5;
    this.resetTimeout = options.resetTimeout || 60000; // 1 minute
    this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
    this.failures = 0;
    this.lastFailureTime = null;
    this.successCount = 0;
  }

  async execute(fn) {
    // Check if circuit is open
    if (this.state === 'OPEN') {
      // Check if enough time has passed to try again
      if (Date.now() - this.lastFailureTime >= this.resetTimeout) {
        this.state = 'HALF_OPEN';
        console.log('Circuit breaker entering HALF_OPEN state');
      } else {
        throw new Error('Circuit breaker is OPEN. Service is unavailable.');
      }
    }

    try {
      const result = await fn();

      // Success - handle based on current state
      if (this.state === 'HALF_OPEN') {
        this.successCount++;

        // After 2 successful requests, close the circuit
        if (this.successCount >= 2) {
          this.reset();
          console.log('Circuit breaker CLOSED after successful requests');
        }
      } else {
        this.reset();
      }

      return result;
    } catch (error) {
      this.failures++;
      this.lastFailureTime = Date.now();

      // Trip the circuit if threshold exceeded
      if (this.failures >= this.failureThreshold) {
        this.state = 'OPEN';
        console.error(`Circuit breaker OPEN after ${this.failures} failures`);
      }

      throw error;
    }
  }

  reset() {
    this.failures = 0;
    this.successCount = 0;
    this.state = 'CLOSED';
  }

  getStatus() {
    return {
      state: this.state,
      failures: this.failures,
      lastFailureTime: this.lastFailureTime
    };
  }
}

// Usage with API client
class APIClientWithCircuitBreaker {
  constructor(apiKey) {
    this.apiKey = apiKey;
    this.circuitBreaker = new CircuitBreaker({
      failureThreshold: 5,
      resetTimeout: 60000
    });
  }

  async request(endpoint, options = {}) {
    return await this.circuitBreaker.execute(async () => {
      const response = await fetch(endpoint, {
        ...options,
        headers: {
          'Authorization': `Bearer ${this.apiKey}`,
          'Content-Type': 'application/json',
          ...options.headers
        }
      });

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
      }

      return await response.json();
    });
  }
}

const client = new APIClientWithCircuitBreaker(API_KEY);

// Circuit breaker protects against cascading failures
try {
  const data = await client.request('https://app.workskedge.com/api/v1/projects');
} catch (error) {
  if (error.message.includes('Circuit breaker is OPEN')) {
    console.log('Service temporarily unavailable. Please try again later.');
  }
}

Graceful Degradation

Implement fallback strategies to keep your application functional even when API requests fail.

class GracefulAPIClient {
  constructor(apiKey, cache) {
    this.apiKey = apiKey;
    this.cache = cache; // External cache (Redis, localStorage, etc.)
  }

  async getWithFallback(cacheKey, fetchFn, options = {}) {
    const {
      maxAge = 300000, // 5 minutes
      useStaleOnError = true
    } = options;

    try {
      // Try to fetch fresh data
      const data = await fetchFn();

      // Cache the successful response
      await this.cache.set(cacheKey, {
        data,
        timestamp: Date.now()
      });

      return {
        data,
        source: 'api',
        fresh: true
      };
    } catch (error) {
      console.error('API request failed:', error);

      if (!useStaleOnError) {
        throw error;
      }

      // Try to use cached data
      const cached = await this.cache.get(cacheKey);

      if (!cached) {
        throw new Error('No cached data available');
      }

      const age = Date.now() - cached.timestamp;
      const isStale = age > maxAge;

      if (isStale) {
        console.warn(`Using stale cached data (${Math.round(age / 1000)}s old)`);
      }

      return {
        data: cached.data,
        source: 'cache',
        fresh: false,
        age
      };
    }
  }

  async getProjects() {
    return this.getWithFallback(
      'projects',
      async () => {
        const response = await fetch(
          'https://app.workskedge.com/api/v1/projects',
          {
            headers: {
              'Authorization': `Bearer ${this.apiKey}`,
              'Content-Type': 'application/json'
            }
          }
        );

        if (!response.ok) {
          throw new Error(`HTTP ${response.status}`);
        }

        return await response.json();
      },
      {
        maxAge: 600000, // 10 minutes
        useStaleOnError: true
      }
    );
  }
}

// Simple in-memory cache implementation
class MemoryCache {
  constructor() {
    this.store = new Map();
  }

  async get(key) {
    return this.store.get(key);
  }

  async set(key, value) {
    this.store.set(key, value);
  }
}

// Usage
const cache = new MemoryCache();
const client = new GracefulAPIClient(API_KEY, cache);

const result = await client.getProjects();

if (result.fresh) {
  console.log('Showing fresh data from API');
} else {
  console.log(`Showing cached data (${Math.round(result.age / 1000)}s old)`);
  console.log('Note: Data may be outdated due to API unavailability');
}

Error Handling Best Practices

Always Set Timeouts

Prevent hanging requests by setting reasonable timeouts (30 seconds recommended)

Log Errors with Context

Include request details, timestamps, and user context (but never API keys)

Use Exponential Backoff

Wait progressively longer between retries to avoid overwhelming the API

Respect Rate Limits

Monitor rate limit headers and implement queuing when necessary

Implement Circuit Breakers

Stop making requests after repeated failures to prevent cascading issues

Provide User-Friendly Messages

Translate technical errors into actionable messages for end users

Implement Fallbacks

Use cached data or graceful degradation when API is unavailable

Monitor and Alert

Set up monitoring for error rates and alert on anomalies

postMessage API Overview

The postMessage API enables bidirectional communication between your embedded app and the WorkSkedge platform. Your app can receive context updates, send notifications, request navigation, and more.

Security: Always verify that event.origin === 'https://app.workskedge.com' before processing messages.

Messages from WorkSkedge → Your App

WorkSkedge sends these messages to your embedded app:

workskedge:context

Inbound

Sent automatically when the token is about to expire or when user context changes. Contains a fresh JWT token.

Message Format

{
  type: 'workskedge:context',
  token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
}

Handler Example

window.addEventListener('message', (event) => {
  if (event.origin !== 'https://app.workskedge.com') return;

  if (event.data.type === 'workskedge:context') {
    const newToken = event.data.token;
    // Update stored token and user context
    auth.setToken(newToken);
    console.log('Token refreshed');
  }
});

Messages from Your App → WorkSkedge

Your app can send these messages to WorkSkedge:

workskedge:ready

Outbound

Notify WorkSkedge that your app has finished loading and is ready to display. This removes the loading spinner.

Message Format

{
  type: 'workskedge:ready'
}

Example

// Send when your app is fully loaded
window.parent.postMessage({
  type: 'workskedge:ready'
}, 'https://app.workskedge.com');

workskedge:request_context

Outbound

Request the current user context and token. WorkSkedge will respond with a workskedge:context message.

Message Format

{
  type: 'workskedge:request_context'
}

Example

// Request fresh token and context
window.parent.postMessage({
  type: 'workskedge:request_context'
}, 'https://app.workskedge.com');

// Listen for response
window.addEventListener('message', (event) => {
  if (event.origin !== 'https://app.workskedge.com') return;

  if (event.data.type === 'workskedge:context') {
    console.log('Received context:', event.data.token);
  }
});

workskedge:notify

Outbound

Display a notification toast in the WorkSkedge interface. Supports success, error, warning, and info types.

Message Format

{
  type: 'workskedge:notify',
  level: 'success' | 'error' | 'warning' | 'info',
  message: 'Notification message text'
}

Example

// Show success notification
window.parent.postMessage({
  type: 'workskedge:notify',
  level: 'success',
  message: 'Settings saved successfully!'
}, 'https://app.workskedge.com');

// Show error notification
window.parent.postMessage({
  type: 'workskedge:notify',
  level: 'error',
  message: 'Failed to save settings. Please try again.'
}, 'https://app.workskedge.com');

workskedge:navigate

Outbound

Navigate the parent WorkSkedge window to a specific path or URL.

Message Format

{
  type: 'workskedge:navigate',
  path: '/projects/123'  // WorkSkedge internal path or external URL
}

Example

// Navigate to a WorkSkedge project
window.parent.postMessage({
  type: 'workskedge:navigate',
  path: '/projects/abc-123-def'
}, 'https://app.workskedge.com');

// Navigate to external URL
window.parent.postMessage({
  type: 'workskedge:navigate',
  path: 'https://example.com/external-page'
}, 'https://app.workskedge.com');

workskedge:refresh

Outbound

Request WorkSkedge to refresh data or the current view. Useful after your app makes changes to WorkSkedge data.

Message Format

{
  type: 'workskedge:refresh'
}

Example

// After creating a new project via API
async function createProject(data) {
  const response = await fetch('/api/projects', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${auth.getToken()}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(data)
  });

  if (response.ok) {
    // Request WorkSkedge to refresh its data
    window.parent.postMessage({
      type: 'workskedge:refresh'
    }, 'https://app.workskedge.com');
  }
}

Complete postMessage Helper

Here's a complete helper class that handles all postMessage communication:

WorkSkedgeMessenger.js JavaScript
class WorkSkedgeMessenger {
  constructor(onContextUpdate) {
    this.WORKSKEDGE_ORIGIN = 'https://app.workskedge.com';
    this.onContextUpdate = onContextUpdate;
    this.setupListener();
  }

  setupListener() {
    window.addEventListener('message', (event) => {
      // Security: Verify origin
      if (event.origin !== this.WORKSKEDGE_ORIGIN) {
        return;
      }

      this.handleMessage(event.data);
    });
  }

  handleMessage(data) {
    switch (data.type) {
      case 'workskedge:context':
        if (this.onContextUpdate) {
          this.onContextUpdate(data.token);
        }
        break;

      default:
        console.log('Unknown message type:', data.type);
    }
  }

  send(type, payload = {}) {
    const message = { type, ...payload };
    window.parent.postMessage(message, this.WORKSKEDGE_ORIGIN);
  }

  ready() {
    this.send('workskedge:ready');
  }

  requestContext() {
    this.send('workskedge:request_context');
  }

  notify(level, message) {
    this.send('workskedge:notify', { level, message });
  }

  success(message) {
    this.notify('success', message);
  }

  error(message) {
    this.notify('error', message);
  }

  warning(message) {
    this.notify('warning', message);
  }

  info(message) {
    this.notify('info', message);
  }

  navigate(path) {
    this.send('workskedge:navigate', { path });
  }

  refresh() {
    this.send('workskedge:refresh');
  }
}

// Usage example
const messenger = new WorkSkedgeMessenger((newToken) => {
  console.log('Token updated:', newToken);
  auth.setToken(newToken);
});

// Notify when app is ready
messenger.ready();

// Show notifications
messenger.success('Data saved successfully!');
messenger.error('Failed to load data');

// Navigate to a page
messenger.navigate('/projects/123');

// Refresh WorkSkedge data
messenger.refresh();

// Request fresh token
messenger.requestContext();

TypeScript Definitions

For TypeScript projects, use these type definitions:

workskedge-types.ts TypeScript
// Messages from WorkSkedge → App
export interface WorkSkedgeContextMessage {
  type: 'workskedge:context';
  token: string;
}

export type InboundMessage = WorkSkedgeContextMessage;

// Messages from App → WorkSkedge
export interface WorkSkedgeReadyMessage {
  type: 'workskedge:ready';
}

export interface WorkSkedgeRequestContextMessage {
  type: 'workskedge:request_context';
}

export interface WorkSkedgeNotifyMessage {
  type: 'workskedge:notify';
  level: 'success' | 'error' | 'warning' | 'info';
  message: string;
}

export interface WorkSkedgeNavigateMessage {
  type: 'workskedge:navigate';
  path: string;
}

export interface WorkSkedgeRefreshMessage {
  type: 'workskedge:refresh';
}

export type OutboundMessage =
  | WorkSkedgeReadyMessage
  | WorkSkedgeRequestContextMessage
  | WorkSkedgeNotifyMessage
  | WorkSkedgeNavigateMessage
  | WorkSkedgeRefreshMessage;

// JWT Token Payload
export interface WorkSkedgeTokenPayload {
  iss: 'workskedge';
  sub: string;
  iat: number;
  exp: number;
  companyId: string;
  installId: string;
  appId: string;
  employeeId: string | null;
  employeeName: string;
  email: string;
  roles: string[];
  nonce: string;
}

Best Practices

Always Verify Origin

Check that event.origin === 'https://app.workskedge.com' before processing any messages.

Send Ready Message

Always send workskedge:ready when your app finishes loading to remove the loading spinner.

Handle Token Updates

Listen for workskedge:context messages to receive fresh tokens before expiration.

Use Notifications Wisely

Only show notifications for important user actions. Avoid spamming with too many notifications.

Request Context on Error

If you encounter auth errors, use workskedge:request_context to get a fresh token.

Refresh After Changes

Send workskedge:refresh after making changes to WorkSkedge data via API.

Common Patterns

App Initialization

// Initialize app with token and postMessage
const auth = new WorkSkedgeAuth();
const messenger = new WorkSkedgeMessenger((newToken) => {
  auth.setToken(newToken);
});

// Get initial token from URL
const urlParams = new URLSearchParams(window.location.search);
const initialToken = urlParams.get('token');

if (initialToken) {
  auth.setToken(initialToken);
  initializeApp(auth.getUser());
  messenger.ready();
} else {
  // No token in URL, request one
  messenger.requestContext();
}

API Call with Token Refresh

async function apiCall(endpoint, options = {}) {
  const token = auth.getToken();

  try {
    const response = await fetch(endpoint, {
      ...options,
      headers: {
        'Authorization': `Bearer ${token}`,
        'Content-Type': 'application/json',
        ...options.headers
      }
    });

    // Handle expired token
    if (response.status === 401) {
      // Request fresh token
      messenger.requestContext();

      // Wait for new token (simplified - use Promise in production)
      await new Promise(resolve => setTimeout(resolve, 1000));

      // Retry with new token
      return apiCall(endpoint, options);
    }

    return response.json();
  } catch (error) {
    messenger.error('Network error. Please try again.');
    throw error;
  }
}

Form Submission with Feedback

async function handleFormSubmit(formData) {
  try {
    const response = await apiCall('/api/save-data', {
      method: 'POST',
      body: JSON.stringify(formData)
    });

    if (response.success) {
      messenger.success('Data saved successfully!');
      messenger.refresh(); // Refresh WorkSkedge data
    } else {
      messenger.error('Failed to save data');
    }
  } catch (error) {
    messenger.error('An error occurred. Please try again.');
  }
}

Testing Your Embedded App

WorkSkedge provides a built-in Dev Launcher that allows you to test your embedded app during development with real JWT tokens and user context, without deploying to production.

Dev Launcher: Access the Dev Launcher from your app's settings page in WorkSkedge to test with real tokens before deployment.

Development vs Production URLs

Your app can have separate URLs for development and production environments:

Environment Example URL Use Case
Development http://localhost:3000 Local testing during development
Staging https://staging.myapp.com Testing before production release
Production https://app.myapp.com Live app for all users
Local Development: To test with http://localhost, you may need to configure your browser to allow mixed content or use a tool like ngrok to create an HTTPS tunnel.

Using the Dev Launcher

The Dev Launcher provides several options for testing your app:

Launch Dev Mode

Load your development URL with a real JWT token and full user context.

Launch Production

Test your production URL to verify deployment before making it live.

Launch Custom URL

Test any URL (staging, feature branches, etc.) with real tokens.

View Token Info

Inspect the current JWT token and decoded payload for debugging.

Development Workflow

Follow this workflow for efficient app development:

1

Start Local Development

Run your app locally (e.g., npm run dev) on localhost:3000

2

Configure Development URL

In your WorkSkedge app settings, set the development URL to http://localhost:3000

3

Launch Dev Mode

Click "Launch Dev Mode" in the Dev Launcher to open your local app with a real token

4

Develop & Test

Make changes to your code and refresh the iframe to test immediately

5

Deploy & Verify

Deploy to production and use "Launch Production" to verify before going live

Debugging Tools

JWT Token Decoder

Use this helper to decode and inspect JWT tokens in your browser console:

Token Decoder JavaScript
function decodeToken(token) {
  try {
    const parts = token.split('.');
    if (parts.length !== 3) {
      throw new Error('Invalid token format');
    }

    const header = JSON.parse(atob(parts[0]));
    const payload = JSON.parse(atob(parts[1]));

    console.log('=== JWT TOKEN DEBUG ===');
    console.log('Header:', header);
    console.log('Payload:', payload);
    console.log('\nUser Info:');
    console.log('  Name:', payload.employeeName);
    console.log('  Email:', payload.email);
    console.log('  Roles:', payload.roles);
    console.log('  Company:', payload.companyId);
    console.log('\nToken Status:');
    const now = Math.floor(Date.now() / 1000);
    const expiresIn = payload.exp - now;
    console.log('  Issued:', new Date(payload.iat * 1000).toLocaleString());
    console.log('  Expires:', new Date(payload.exp * 1000).toLocaleString());
    console.log('  Expires in:', Math.max(0, expiresIn), 'seconds');
    console.log('  Is Valid:', expiresIn > 0);

    return { header, payload, expiresIn };
  } catch (error) {
    console.error('Failed to decode token:', error);
    return null;
  }
}

// Usage in browser console:
// Get token from URL
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get('token');
decodeToken(token);

postMessage Debugger

Log all postMessage communication for debugging:

Message Logger JavaScript
// Enable postMessage debugging
function enablePostMessageDebug() {
  console.log('=== postMessage Debug Mode Enabled ===');

  // Log all incoming messages
  window.addEventListener('message', (event) => {
    console.log('📨 Received message:', {
      origin: event.origin,
      type: event.data.type,
      data: event.data,
      timestamp: new Date().toISOString()
    });
  });

  // Intercept and log all outgoing messages
  const originalPostMessage = window.parent.postMessage.bind(window.parent);
  window.parent.postMessage = function(message, targetOrigin, transfer) {
    console.log('📤 Sending message:', {
      type: message.type,
      message: message,
      targetOrigin: targetOrigin,
      timestamp: new Date().toISOString()
    });
    return originalPostMessage(message, targetOrigin, transfer);
  };

  console.log('All postMessage events will be logged to console');
}

// Enable in development
if (process.env.NODE_ENV === 'development') {
  enablePostMessageDebug();
}

Token Refresh Monitor

Monitor token expiration and refresh timing:

Token Monitor JavaScript
class TokenMonitor {
  constructor(token) {
    this.setToken(token);
    this.startMonitoring();
  }

  setToken(token) {
    this.token = token;
    try {
      const payload = JSON.parse(atob(token.split('.')[1]));
      this.expiry = payload.exp;
      console.log('Token updated. Expires:', new Date(this.expiry * 1000).toLocaleString());
    } catch (error) {
      console.error('Invalid token:', error);
    }
  }

  startMonitoring() {
    setInterval(() => {
      if (!this.expiry) return;

      const now = Math.floor(Date.now() / 1000);
      const remaining = this.expiry - now;

      if (remaining <= 0) {
        console.warn('⚠️ Token expired!');
      } else if (remaining <= 300) {
        console.warn(`⏰ Token expires in ${remaining} seconds`);
      }
    }, 30000); // Check every 30 seconds
  }
}

// Usage
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get('token');
const monitor = new TokenMonitor(token);

// Update monitor when new token arrives
window.addEventListener('message', (event) => {
  if (event.origin === 'https://app.workskedge.com' &&
      event.data.type === 'workskedge:context') {
    monitor.setToken(event.data.token);
  }
});

Testing Checklist

Before deploying to production, verify the following:

App loads successfully with JWT token from URL parameter
Token is decoded correctly and user information displays properly
Token verification succeeds on your backend server
postMessage listener receives workskedge:context updates
workskedge:ready message sent after app initialization
Notifications display correctly using workskedge:notify
Navigation works with workskedge:navigate messages
Token refresh handled gracefully before expiration
Expired token errors handled with appropriate user feedback
Role-based permissions work correctly for different user types
App served over HTTPS in production (required for iframe embedding)

Common Issues & Solutions

Token not found in URL

The token parameter is missing from the URL when your app loads.

Solution:
  • Send workskedge:request_context message to get the token via postMessage
  • Verify your app URL is correctly configured in WorkSkedge settings
  • Check browser console for any URL parsing errors

Token verification fails

Server-side verification returns valid: false.

Solution:
  • Check that the token hasn't expired (tokens are valid for 1 hour)
  • Ensure you're sending the complete token string, including all three parts
  • Verify your server is correctly calling the verification endpoint
  • Check for network connectivity issues to WorkSkedge API

postMessage not working

Your app doesn't receive messages from WorkSkedge.

Solution:
  • Verify origin check: event.origin === 'https://app.workskedge.com'
  • Ensure event listener is set up before messages are sent
  • Check browser console for CORS or iframe sandbox restrictions
  • Use the postMessage debugger (above) to log all messages

Mixed content errors

Browser blocks loading http://localhost in HTTPS iframe.

Solution:
  • Use a tool like ngrok to create an HTTPS tunnel
  • Configure your browser to allow mixed content for development
  • Use a local HTTPS certificate for your development server

Complete Code Examples

This page provides complete, working examples of embedded apps in various frameworks and languages. All examples include JWT authentication, postMessage communication, and best practices.

Standalone HTML App

A complete embedded app built with vanilla JavaScript and HTML:

index.html HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>WorkSkedge Embedded App</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
      background: #f5f5f5;
      padding: 2rem;
    }
    .container { max-width: 800px; margin: 0 auto; }
    .card {
      background: white;
      border-radius: 8px;
      padding: 2rem;
      margin-bottom: 1.5rem;
      box-shadow: 0 1px 3px rgba(0,0,0,0.1);
    }
    .header { border-bottom: 2px solid #14b8a6; padding-bottom: 1rem; margin-bottom: 1.5rem; }
    .user-info { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; }
    .info-item { padding: 0.75rem; background: #f9fafb; border-radius: 6px; }
    .info-label { font-size: 0.875rem; color: #6b7280; margin-bottom: 0.25rem; }
    .info-value { font-weight: 600; color: #111827; }
    .roles { display: flex; flex-wrap: wrap; gap: 0.5rem; }
    .role-badge {
      display: inline-block;
      padding: 0.25rem 0.75rem;
      background: #14b8a6;
      color: white;
      border-radius: 4px;
      font-size: 0.875rem;
    }
    button {
      padding: 0.75rem 1.5rem;
      background: #14b8a6;
      color: white;
      border: none;
      border-radius: 6px;
      font-size: 1rem;
      cursor: pointer;
      margin-right: 0.5rem;
      margin-bottom: 0.5rem;
    }
    button:hover { background: #0f9b8e; }
    #status {
      padding: 1rem;
      border-radius: 6px;
      margin-top: 1rem;
      display: none;
    }
    .status-success { background: #d1fae5; color: #065f46; display: block; }
    .status-error { background: #fee2e2; color: #991b1b; display: block; }
  </style>
</head>
<body>
  <div class="container">
    <div class="card">
      <div class="header">
        <h1>WorkSkedge Embedded App Example</h1>
      </div>

      <div id="user-section" style="display: none;">
        <h2>User Information</h2>
        <div class="user-info">
          <div class="info-item">
            <div class="info-label">Name</div>
            <div class="info-value" id="user-name">-</div>
          </div>
          <div class="info-item">
            <div class="info-label">Email</div>
            <div class="info-value" id="user-email">-</div>
          </div>
          <div class="info-item">
            <div class="info-label">Company ID</div>
            <div class="info-value" id="user-company">-</div>
          </div>
          <div class="info-item">
            <div class="info-label">Employee ID</div>
            <div class="info-value" id="user-employee">-</div>
          </div>
        </div>

        <div style="margin-top: 1.5rem;">
          <div class="info-label">Roles</div>
          <div class="roles" id="user-roles"></div>
        </div>
      </div>
    </div>

    <div class="card">
      <h2>Actions</h2>
      <button onclick="app.showSuccessNotification()">Show Success</button>
      <button onclick="app.showErrorNotification()">Show Error</button>
      <button onclick="app.requestFreshToken()">Request Fresh Token</button>
      <button onclick="app.navigateToProjects()">Navigate to Projects</button>
      <div id="status"></div>
    </div>
  </div>

  <script>
    class WorkSkedgeApp {
      constructor() {
        this.WORKSKEDGE_ORIGIN = 'https://app.workskedge.com';
        this.token = null;
        this.user = null;
        this.init();
      }

      init() {
        this.setupPostMessage();
        this.getInitialToken();
      }

      setupPostMessage() {
        window.addEventListener('message', (event) => {
          if (event.origin !== this.WORKSKEDGE_ORIGIN) return;

          if (event.data.type === 'workskedge:context') {
            this.handleTokenUpdate(event.data.token);
          }
        });
      }

      getInitialToken() {
        const urlParams = new URLSearchParams(window.location.search);
        const token = urlParams.get('token');

        if (token) {
          this.handleTokenUpdate(token);
          this.sendReady();
        } else {
          this.requestFreshToken();
        }
      }

      handleTokenUpdate(token) {
        this.token = token;

        try {
          const payload = JSON.parse(atob(token.split('.')[1]));
          this.user = {
            id: payload.sub,
            name: payload.employeeName,
            email: payload.email,
            companyId: payload.companyId,
            employeeId: payload.employeeId,
            roles: payload.roles
          };

          this.updateUI();
          this.showStatus('Token updated successfully', 'success');
        } catch (error) {
          console.error('Failed to decode token:', error);
          this.showStatus('Failed to decode token', 'error');
        }
      }

      updateUI() {
        document.getElementById('user-section').style.display = 'block';
        document.getElementById('user-name').textContent = this.user.name;
        document.getElementById('user-email').textContent = this.user.email;
        document.getElementById('user-company').textContent = this.user.companyId;
        document.getElementById('user-employee').textContent = this.user.employeeId || 'N/A';

        const rolesContainer = document.getElementById('user-roles');
        rolesContainer.innerHTML = '';
        this.user.roles.forEach(role => {
          const badge = document.createElement('span');
          badge.className = 'role-badge';
          badge.textContent = role;
          rolesContainer.appendChild(badge);
        });
      }

      sendMessage(type, payload = {}) {
        window.parent.postMessage(
          { type, ...payload },
          this.WORKSKEDGE_ORIGIN
        );
      }

      sendReady() {
        this.sendMessage('workskedge:ready');
      }

      requestFreshToken() {
        this.sendMessage('workskedge:request_context');
        this.showStatus('Requesting fresh token...', 'success');
      }

      showSuccessNotification() {
        this.sendMessage('workskedge:notify', {
          level: 'success',
          message: 'This is a success notification!'
        });
      }

      showErrorNotification() {
        this.sendMessage('workskedge:notify', {
          level: 'error',
          message: 'This is an error notification!'
        });
      }

      navigateToProjects() {
        this.sendMessage('workskedge:navigate', {
          path: '/projects'
        });
      }

      showStatus(message, type) {
        const status = document.getElementById('status');
        status.textContent = message;
        status.className = `status-${type}`;
        setTimeout(() => {
          status.style.display = 'none';
        }, 3000);
      }
    }

    const app = new WorkSkedgeApp();
  </script>
</body>
</html>

React App Example

Complete React component with hooks for authentication and postMessage:

App.jsx JavaScript
import React, { useState, useEffect, useCallback } from 'react';

const WORKSKEDGE_ORIGIN = 'https://app.workskedge.com';

function App() {
  const [token, setToken] = useState(null);
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  const sendMessage = useCallback((type, payload = {}) => {
    window.parent.postMessage(
      { type, ...payload },
      WORKSKEDGE_ORIGIN
    );
  }, []);

  const decodeToken = useCallback((token) => {
    try {
      const payload = JSON.parse(atob(token.split('.')[1]));
      return {
        id: payload.sub,
        name: payload.employeeName,
        email: payload.email,
        companyId: payload.companyId,
        employeeId: payload.employeeId,
        roles: payload.roles,
        exp: payload.exp
      };
    } catch (error) {
      console.error('Failed to decode token:', error);
      return null;
    }
  }, []);

  const handleTokenUpdate = useCallback((newToken) => {
    setToken(newToken);
    const userData = decodeToken(newToken);
    setUser(userData);
    setLoading(false);
  }, [decodeToken]);

  useEffect(() => {
    const handleMessage = (event) => {
      if (event.origin !== WORKSKEDGE_ORIGIN) return;

      if (event.data.type === 'workskedge:context') {
        handleTokenUpdate(event.data.token);
      }
    };

    window.addEventListener('message', handleMessage);

    const urlParams = new URLSearchParams(window.location.search);
    const initialToken = urlParams.get('token');

    if (initialToken) {
      handleTokenUpdate(initialToken);
      sendMessage('workskedge:ready');
    } else {
      sendMessage('workskedge:request_context');
    }

    return () => window.removeEventListener('message', handleMessage);
  }, [handleTokenUpdate, sendMessage]);

  const showNotification = (level, message) => {
    sendMessage('workskedge:notify', { level, message });
  };

  const navigateToProjects = () => {
    sendMessage('workskedge:navigate', { path: '/projects' });
  };

  if (loading) {
    return (
      <div style={{ padding: '2rem', textAlign: 'center' }}>
        Loading...
      </div>
    );
  }

  return (
    <div style={{ padding: '2rem', maxWidth: '800px', margin: '0 auto' }}>
      <div style={{
        background: 'white',
        borderRadius: '8px',
        padding: '2rem',
        marginBottom: '1.5rem',
        boxShadow: '0 1px 3px rgba(0,0,0,0.1)'
      }}>
        <h1 style={{ borderBottom: '2px solid #14b8a6', paddingBottom: '1rem', marginBottom: '1.5rem' }}>
          React Embedded App
        </h1>

        <h2>User Information</h2>
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '1rem', marginTop: '1rem' }}>
          <div style={{ padding: '0.75rem', background: '#f9fafb', borderRadius: '6px' }}>
            <div style={{ fontSize: '0.875rem', color: '#6b7280' }}>Name</div>
            <div style={{ fontWeight: 600 }}>{user.name}</div>
          </div>
          <div style={{ padding: '0.75rem', background: '#f9fafb', borderRadius: '6px' }}>
            <div style={{ fontSize: '0.875rem', color: '#6b7280' }}>Email</div>
            <div style={{ fontWeight: 600 }}>{user.email}</div>
          </div>
          <div style={{ padding: '0.75rem', background: '#f9fafb', borderRadius: '6px' }}>
            <div style={{ fontSize: '0.875rem', color: '#6b7280' }}>Company ID</div>
            <div style={{ fontWeight: 600, fontSize: '0.75rem' }}>{user.companyId}</div>
          </div>
        </div>

        <div style={{ marginTop: '1.5rem' }}>
          <div style={{ fontSize: '0.875rem', color: '#6b7280', marginBottom: '0.5rem' }}>Roles</div>
          <div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem' }}>
            {user.roles.map(role => (
              <span key={role} style={{
                padding: '0.25rem 0.75rem',
                background: '#14b8a6',
                color: 'white',
                borderRadius: '4px',
                fontSize: '0.875rem'
              }}>
                {role}
              </span>
            ))}
          </div>
        </div>
      </div>

      <div style={{
        background: 'white',
        borderRadius: '8px',
        padding: '2rem',
        boxShadow: '0 1px 3px rgba(0,0,0,0.1)'
      }}>
        <h2>Actions</h2>
        <div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem', marginTop: '1rem' }}>
          <button
            onClick={() => showNotification('success', 'Success notification!')}
            style={{
              padding: '0.75rem 1.5rem',
              background: '#14b8a6',
              color: 'white',
              border: 'none',
              borderRadius: '6px',
              cursor: 'pointer'
            }}
          >
            Show Success
          </button>
          <button
            onClick={() => showNotification('error', 'Error notification!')}
            style={{
              padding: '0.75rem 1.5rem',
              background: '#14b8a6',
              color: 'white',
              border: 'none',
              borderRadius: '6px',
              cursor: 'pointer'
            }}
          >
            Show Error
          </button>
          <button
            onClick={navigateToProjects}
            style={{
              padding: '0.75rem 1.5rem',
              background: '#14b8a6',
              color: 'white',
              border: 'none',
              borderRadius: '6px',
              cursor: 'pointer'
            }}
          >
            Navigate to Projects
          </button>
        </div>
      </div>
    </div>
  );
}

export default App;

Vue 3 App Example

Complete Vue 3 component with Composition API:

App.vue JavaScript
<template>
  <div class="container">
    <div v-if="loading" class="loading">Loading...</div>

    <div v-else>
      <div class="card">
        <h1 class="header">Vue 3 Embedded App</h1>

        <h2>User Information</h2>
        <div class="user-info">
          <div class="info-item">
            <div class="info-label">Name</div>
            <div class="info-value">{{ user.name }}</div>
          </div>
          <div class="info-item">
            <div class="info-label">Email</div>
            <div class="info-value">{{ user.email }}</div>
          </div>
          <div class="info-item">
            <div class="info-label">Company ID</div>
            <div class="info-value">{{ user.companyId }}</div>
          </div>
        </div>

        <div class="roles-section">
          <div class="info-label">Roles</div>
          <div class="roles">
            <span v-for="role in user.roles" :key="role" class="role-badge">
              {{ role }}
            </span>
          </div>
        </div>
      </div>

      <div class="card">
        <h2>Actions</h2>
        <div class="actions">
          <button @click="showSuccess">Show Success</button>
          <button @click="showError">Show Error</button>
          <button @click="navigateToProjects">Navigate to Projects</button>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue';

const WORKSKEDGE_ORIGIN = 'https://app.workskedge.com';

const token = ref(null);
const user = ref(null);
const loading = ref(true);

const sendMessage = (type, payload = {}) => {
  window.parent.postMessage(
    { type, ...payload },
    WORKSKEDGE_ORIGIN
  );
};

const decodeToken = (token) => {
  try {
    const payload = JSON.parse(atob(token.split('.')[1]));
    return {
      id: payload.sub,
      name: payload.employeeName,
      email: payload.email,
      companyId: payload.companyId,
      employeeId: payload.employeeId,
      roles: payload.roles
    };
  } catch (error) {
    console.error('Failed to decode token:', error);
    return null;
  }
};

const handleTokenUpdate = (newToken) => {
  token.value = newToken;
  user.value = decodeToken(newToken);
  loading.value = false;
};

const handleMessage = (event) => {
  if (event.origin !== WORKSKEDGE_ORIGIN) return;

  if (event.data.type === 'workskedge:context') {
    handleTokenUpdate(event.data.token);
  }
};

const showSuccess = () => {
  sendMessage('workskedge:notify', {
    level: 'success',
    message: 'Success notification from Vue!'
  });
};

const showError = () => {
  sendMessage('workskedge:notify', {
    level: 'error',
    message: 'Error notification from Vue!'
  });
};

const navigateToProjects = () => {
  sendMessage('workskedge:navigate', { path: '/projects' });
};

onMounted(() => {
  window.addEventListener('message', handleMessage);

  const urlParams = new URLSearchParams(window.location.search);
  const initialToken = urlParams.get('token');

  if (initialToken) {
    handleTokenUpdate(initialToken);
    sendMessage('workskedge:ready');
  } else {
    sendMessage('workskedge:request_context');
  }
});

onUnmounted(() => {
  window.removeEventListener('message', handleMessage);
});
</script>

<style scoped>
.container { padding: 2rem; max-width: 800px; margin: 0 auto; }
.loading { text-align: center; padding: 2rem; }
.card {
  background: white;
  border-radius: 8px;
  padding: 2rem;
  margin-bottom: 1.5rem;
  box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.header {
  border-bottom: 2px solid #14b8a6;
  padding-bottom: 1rem;
  margin-bottom: 1.5rem;
}
.user-info {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 1rem;
  margin-top: 1rem;
}
.info-item {
  padding: 0.75rem;
  background: #f9fafb;
  border-radius: 6px;
}
.info-label {
  font-size: 0.875rem;
  color: #6b7280;
  margin-bottom: 0.25rem;
}
.info-value {
  font-weight: 600;
}
.roles-section {
  margin-top: 1.5rem;
}
.roles {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 0.5rem;
}
.role-badge {
  padding: 0.25rem 0.75rem;
  background: #14b8a6;
  color: white;
  border-radius: 4px;
  font-size: 0.875rem;
}
.actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 1rem;
}
button {
  padding: 0.75rem 1.5rem;
  background: #14b8a6;
  color: white;
  border: none;
  border-radius: 6px;
  cursor: pointer;
}
button:hover {
  background: #0f9b8e;
}
</style>

Node.js Backend Example

Express.js server with token verification middleware:

server.js JavaScript
const express = require('express');
const axios = require('axios');
const cors = require('cors');

const app = express();
app.use(express.json());
app.use(cors());

const VERIFICATION_URL = 'https://app.workskedge.com/api/apps-verify-embed-token';

async function verifyToken(token) {
  try {
    const response = await axios.post(
      VERIFICATION_URL,
      { token },
      { headers: { 'Content-Type': 'application/json' } }
    );
    return response.data;
  } catch (error) {
    console.error('Token verification failed:', error.message);
    return { valid: false, error: 'Verification failed' };
  }
}

const authenticateToken = async (req, res, next) => {
  const authHeader = req.headers.authorization;
  const token = authHeader?.replace('Bearer ', '');

  if (!token) {
    return res.status(401).json({ error: 'No token provided' });
  }

  const verification = await verifyToken(token);

  if (!verification.valid) {
    return res.status(401).json({ error: 'Invalid or expired token' });
  }

  req.user = verification.payload;
  next();
};

const requireRole = (roles) => {
  return (req, res, next) => {
    const hasRole = roles.some(role => req.user.roles.includes(role));
    if (!hasRole) {
      return res.status(403).json({ error: 'Insufficient permissions' });
    }
    next();
  };
};

app.get('/api/user', authenticateToken, (req, res) => {
  res.json({
    id: req.user.sub,
    name: req.user.employeeName,
    email: req.user.email,
    companyId: req.user.companyId,
    roles: req.user.roles
  });
});

app.post('/api/projects', authenticateToken, requireRole(['administrator', 'manager']), async (req, res) => {
  try {
    console.log('Creating project for user:', req.user.employeeName);

    res.json({
      success: true,
      message: 'Project created',
      projectId: 'abc-123'
    });
  } catch (error) {
    res.status(500).json({ error: 'Failed to create project' });
  }
});

app.get('/api/data', authenticateToken, (req, res) => {
  res.json({
    message: 'Protected data',
    companyId: req.user.companyId,
    data: [
      { id: 1, name: 'Item 1' },
      { id: 2, name: 'Item 2' }
    ]
  });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Python Backend Example

Flask application with token verification:

app.py Python
from flask import Flask, request, jsonify
from flask_cors import CORS
from functools import wraps
import requests

app = Flask(__name__)
CORS(app)

VERIFICATION_URL = 'https://app.workskedge.com/api/apps-verify-embed-token'

def verify_token(token):
    try:
        response = requests.post(
            VERIFICATION_URL,
            json={'token': token},
            headers={'Content-Type': 'application/json'}
        )
        return response.json()
    except Exception as e:
        print(f'Token verification failed: {e}')
        return {'valid': False, 'error': 'Verification failed'}

def authenticate_token(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        auth_header = request.headers.get('Authorization', '')
        token = auth_header.replace('Bearer ', '')

        if not token:
            return jsonify({'error': 'No token provided'}), 401

        verification = verify_token(token)

        if not verification.get('valid'):
            return jsonify({'error': 'Invalid or expired token'}), 401

        request.user = verification['payload']
        return f(*args, **kwargs)

    return decorated_function

def require_role(*roles):
    def decorator(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            user_roles = request.user.get('roles', [])
            has_role = any(role in user_roles for role in roles)

            if not has_role:
                return jsonify({'error': 'Insufficient permissions'}), 403

            return f(*args, **kwargs)
        return decorated_function
    return decorator

@app.route('/api/user', methods=['GET'])
@authenticate_token
def get_user():
    user = request.user
    return jsonify({
        'id': user['sub'],
        'name': user['employeeName'],
        'email': user['email'],
        'companyId': user['companyId'],
        'roles': user['roles']
    })

@app.route('/api/projects', methods=['POST'])
@authenticate_token
@require_role('administrator', 'manager')
def create_project():
    try:
        data = request.json
        print(f"Creating project for user: {request.user['employeeName']}")

        return jsonify({
            'success': True,
            'message': 'Project created',
            'projectId': 'abc-123'
        })
    except Exception as e:
        return jsonify({'error': 'Failed to create project'}), 500

@app.route('/api/data', methods=['GET'])
@authenticate_token
def get_data():
    return jsonify({
        'message': 'Protected data',
        'companyId': request.user['companyId'],
        'data': [
            {'id': 1, 'name': 'Item 1'},
            {'id': 2, 'name': 'Item 2'}
        ]
    })

if __name__ == '__main__':
    app.run(port=3000, debug=True)

PHP Backend Example

PHP script with token verification:

api.php PHP
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');

if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    http_response_code(200);
    exit;
}

define('VERIFICATION_URL', 'https://app.workskedge.com/api/apps-verify-embed-token');

function verifyToken($token) {
    $ch = curl_init(VERIFICATION_URL);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(['token' => $token]));
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);

    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($httpCode !== 200) {
        return ['valid' => false, 'error' => 'Verification failed'];
    }

    return json_decode($response, true);
}

function authenticateRequest() {
    $headers = getallheaders();
    $authHeader = $headers['Authorization'] ?? '';
    $token = str_replace('Bearer ', '', $authHeader);

    if (empty($token)) {
        http_response_code(401);
        echo json_encode(['error' => 'No token provided']);
        exit;
    }

    $verification = verifyToken($token);

    if (!$verification['valid']) {
        http_response_code(401);
        echo json_encode(['error' => 'Invalid or expired token']);
        exit;
    }

    return $verification['payload'];
}

function hasRole($user, $roles) {
    $userRoles = $user['roles'] ?? [];
    foreach ($roles as $role) {
        if (in_array($role, $userRoles)) {
            return true;
        }
    }
    return false;
}

$user = authenticateRequest();

$requestUri = $_SERVER['REQUEST_URI'];
$requestMethod = $_SERVER['REQUEST_METHOD'];

if ($requestMethod === 'GET' && strpos($requestUri, '/api/user') !== false) {
    echo json_encode([
        'id' => $user['sub'],
        'name' => $user['employeeName'],
        'email' => $user['email'],
        'companyId' => $user['companyId'],
        'roles' => $user['roles']
    ]);
    exit;
}

if ($requestMethod === 'POST' && strpos($requestUri, '/api/projects') !== false) {
    if (!hasRole($user, ['administrator', 'manager'])) {
        http_response_code(403);
        echo json_encode(['error' => 'Insufficient permissions']);
        exit;
    }

    echo json_encode([
        'success' => true,
        'message' => 'Project created',
        'projectId' => 'abc-123'
    ]);
    exit;
}

if ($requestMethod === 'GET' && strpos($requestUri, '/api/data') !== false) {
    echo json_encode([
        'message' => 'Protected data',
        'companyId' => $user['companyId'],
        'data' => [
            ['id' => 1, 'name' => 'Item 1'],
            ['id' => 2, 'name' => 'Item 2']
        ]
    ]);
    exit;
}

http_response_code(404);
echo json_encode(['error' => 'Not found']);
?>

Example Resources

All Examples Work Out of the Box

Copy any example and start building. They include authentication, postMessage, and error handling.

Test with Dev Launcher

Use the Dev Launcher to test your app locally with real JWT tokens before deployment.

Read the Guides

Learn best practices for authentication, postMessage communication, and testing.