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.
Log into WorkSkedge
Navigate to app.workskedge.com and sign in to your account.
Access Settings
Click your profile icon in the top right, then select Settings from the dropdown menu.
Navigate to API Keys
In the settings sidebar, click on API Keys to view your API key management page.
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);
}
Next Steps
Now that you've made your first API requests, explore these resources to build more complex integrations:
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.
Navigate to Webhooks Settings
Log into WorkSkedge, go to Settings > Webhooks.
Add New Webhook
Click Add Webhook and enter your endpoint URL (e.g., https://yourdomain.com/webhooks/workskedge).
Select Events
Choose which events you want to receive. Start with a few common events like work_order.created and work_order.updated.
Save Webhook Secret
WorkSkedge will generate a webhook secret. Save this securely as an environment variable.
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.
Install ngrok
# Download from https://ngrok.com or use package manager
brew install ngrok # macOS
# or
npm install -g ngrok # npm
Start Your Local Server
node server.js # Start your webhook endpoint on port 3000
Create ngrok Tunnel
ngrok http 3000
ngrok will give you a public URL like https://abc123.ngrok.io
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
});
}
Next Steps
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
Next Steps
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
Next Steps
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.
event.origin === 'https://app.workskedge.com' before processing messages.
Messages from WorkSkedge → Your App
WorkSkedge sends these messages to your embedded app:
Messages from Your App → WorkSkedge
Your app can send these messages to WorkSkedge:
Complete postMessage Helper
Here's a complete helper class that handles all postMessage communication:
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:
// 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.
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 |
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:
Start Local Development
Run your app locally (e.g., npm run dev) on localhost:3000
Configure Development URL
In your WorkSkedge app settings, set the development URL to http://localhost:3000
Launch Dev Mode
Click "Launch Dev Mode" in the Dev Launcher to open your local app with a real token
Develop & Test
Make changes to your code and refresh the iframe to test immediately
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:
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:
// 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:
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:
workskedge:context updates
workskedge:ready message sent after app initialization
workskedge:notify
workskedge:navigate messages
Common Issues & Solutions
Token not found in URL
The token parameter is missing from the URL when your app loads.
- Send
workskedge:request_contextmessage 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.
- 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.
- 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.
- 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:
<!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:
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:
<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:
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:
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:
<?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']);
?>