Skip to main content

Authentication

Store Credentials Securely

Never hard-code API credentials in your source code!
// Use environment variables
const appSecret = process.env.DUKKANGO_APP_SECRET;
const restSecret = process.env.DUKKANGO_REST_SECRET;

// Or secure key management
const credentials = await keyVault.getSecret('dukkango-api-keys');

Implement Token Refresh

Don’t wait for tokens to expire. Refresh them before expiration.
class TokenManager {
  constructor() {
    this.token = null;
    this.expiresAt = null;
  }
  
  async getToken() {
    // Refresh if expired or expiring within 1 hour
    if (!this.token || Date.now() > this.expiresAt - 3600000) {
      await this.refresh();
    }
    return this.token;
  }
  
  async refresh() {
    const response = await login();
    this.token = response.access_token;
    this.expiresAt = new Date(response.expiration_date).getTime();
  }
}
Always catch authentication errors and retry after refreshing token.
async function apiCall(endpoint, options) {
  try {
    return await fetch(endpoint, {
      ...options,
      headers: {
        ...options.headers,
        'Access-Token': await tokenManager.getToken()
      }
    });
  } catch (error) {
    if (error.status === 401) {
      // Force refresh and retry once
      await tokenManager.refresh();
      return await fetch(endpoint, {
        ...options,
        headers: {
          ...options.headers,
          'Access-Token': await tokenManager.getToken()
        }
      });
    }
    throw error;
  }
}

Order Management

Always Acknowledge Orders

1

Fetch Order

GET /orders/get-current
2

Import to POS

Store order in your local database/system
3

Acknowledge Immediately

PUT /orders/success/{payment_key}
Don’t wait! Acknowledge right after importing.
If you don’t acknowledge orders, they will repeat in every /get-current call, causing duplicate imports!

Use Correct IDs

// Use payment_key (UUID) for API operations
const order = await getOrders();
await acceptOrder(order.payment_key);  // ✅
await successOrder(order.payment_key); // ✅
await completeOrder(order.payment_key); // ✅
Display: Use id (integer) for showing to users
API Calls: Use payment_key (UUID) for all operations

Handle All Statuses

Your POS should gracefully handle orders in any status:
function handleOrder(order) {
  switch (order.status_id) {
    case 1: // RECEIVED
      return displayNewOrder(order);
    
    case 2: // CONFIRMED
      return displayPreparingOrder(order);
    
    case 16: // IN_DELIVERY
      return displayDeliveryOrder(order);
    
    case 5: // COMPLETE
      return archiveOrder(order);
    
    case 6: // CANCELED
      return markCanceled(order);
    
    default:
      console.warn(`Unknown status: ${order.status_id}`);
      return displayOrder(order); // Fallback
  }
}

Polling Strategy

Respect Rate Limits

/orders/get-current and /foods/get-foods have 30-second cooldowns.
// ✅ Good: Poll every 30+ seconds
setInterval(pollOrders, 30000);

// ❌ Bad: Too frequent
setInterval(pollOrders, 5000); // Will hit rate limit!

Implement Exponential Backoff

async function pollWithBackoff() {
  let delay = 30000; // Start with 30s
  const maxDelay = 300000; // Max 5 minutes
  
  while (true) {
    try {
      await pollOrders();
      delay = 30000; // Reset on success
    } catch (error) {
      if (error.status === 429) {
        // Rate limited - increase delay
        delay = Math.min(delay * 2, maxDelay);
        console.log(`Rate limited. Waiting ${delay}ms`);
      }
    }
    
    await sleep(delay);
  }
}

Cache Menu Data

Don’t fetch the full menu on every order! Cache it locally.
class MenuCache {
  constructor() {
    this.menu = null;
    this.lastFetch = null;
    this.cacheDuration = 30 * 60 * 1000; // 30 minutes
  }
  
  async getMenu() {
    const now = Date.now();
    
    // Return cached if recent
    if (this.menu && (now - this.lastFetch) < this.cacheDuration) {
      return this.menu;
    }
    
    // Fetch fresh menu
    this.menu = await fetchMenu();
    this.lastFetch = now;
    return this.menu;
  }
  
  invalidate() {
    this.menu = null;
    this.lastFetch = null;
  }
}

Error Handling

Implement Comprehensive Error Handling

async function makeApiCall(endpoint, options) {
  try {
    const response = await fetch(endpoint, options);
    const data = await response.json();
    
    if (!data.status) {
      // API returned error
      throw new ApiError(data.error, data.message, response.status);
    }
    
    return data.data;
  } catch (error) {
    // Handle specific errors
    if (error instanceof ApiError) {
      handleApiError(error);
    } else if (error.name === 'TypeError') {
      // Network error
      console.error('Network error:', error);
      throw new Error('Unable to connect to API');
    } else {
      // Unknown error
      console.error('Unexpected error:', error);
      throw error;
    }
  }
}

function handleApiError(error) {
  switch (error.status) {
    case 401:
      // Token expired - refresh and retry
      return refreshTokenAndRetry();
    
    case 429:
      // Rate limited - wait and retry
      return waitAndRetry(30000);
    
    case 404:
      // Not found - log and notify
      logError('Resource not found', error);
      notifyUser('Order not found');
      break;
    
    case 422:
      // Validation error - fix and retry
      logError('Validation failed', error);
      notifyUser('Invalid request');
      break;
    
    default:
      // Other errors
      logError('API error', error);
      notifyUser('Something went wrong');
  }
}

Log Errors with Context

function logError(message, error, context = {}) {
  console.error({
    message,
    error: error.message,
    stack: error.stack,
    timestamp: new Date().toISOString(),
    context: {
      ...context,
      userId: currentUser?.id,
      restaurantId: currentRestaurant?.id
    }
  });
  
  // Send to error tracking service
  errorTracker.captureException(error, context);
}

Data Validation

Validate Before Sending

function validateAcceptRequest(preparingTime) {
  if (typeof preparingTime !== 'number') {
    throw new Error('preparing_time must be a number');
  }
  
  if (preparingTime < 5 || preparingTime > 120) {
    throw new Error('preparing_time must be between 5 and 120 minutes');
  }
  
  return true;
}

// Use before API call
validateAcceptRequest(preparingTime);
await acceptOrder(orderId, preparingTime);

Validate Received Data

function validateOrder(order) {
  const required = ['id', 'payment_key', 'status_id', 'customer', 'foods'];
  
  for (const field of required) {
    if (!(field in order)) {
      throw new Error(`Missing required field: ${field}`);
    }
  }
  
  // Validate UUIDs
  const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
  if (!uuidRegex.test(order.payment_key)) {
    throw new Error('Invalid payment_key format');
  }
  
  return true;
}

Performance

Use Connection Pooling

// Reuse HTTP connections
const agent = new https.Agent({
  keepAlive: true,
  maxSockets: 10
});

const apiClient = axios.create({
  httpsAgent: agent,
  timeout: 30000
});

Implement Request Timeouts

async function apiCallWithTimeout(endpoint, options, timeout = 30000) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeout);
  
  try {
    const response = await fetch(endpoint, {
      ...options,
      signal: controller.signal
    });
    return response;
  } finally {
    clearTimeout(timeoutId);
  }
}

Batch Operations When Possible

// ❌ Bad: Update items one by one
for (const item of menuItems) {
  await updateItemStatus(item.id, 'passive');
}

// ✅ Good: Collect and process in batches
const updates = menuItems.map(item => ({
  id: item.id,
  status: 'passive'
}));

// Process updates efficiently
await batchUpdateItems(updates);

Testing

Test All Scenarios

  • Place test order
  • Accept order
  • Mark on the way
  • Complete order
  • Verify status at each step
  • Invalid credentials
  • Expired token
  • Rate limiting
  • Network timeouts
  • Invalid order IDs
  • Missing required fields
  • Orders with no modifiers
  • Orders with multiple modifier groups
  • Scheduled orders
  • Cash vs card payments
  • Different courier types
  • Order repetition after status change

Test Environment

const config = {
  development: {
    baseUrl: 'https://posapitest.fuudy.co',
    logLevel: 'debug'
  },
  production: {
    baseUrl: 'https://www.xn--dkkango-n2a.com/api/integrations',
    logLevel: 'error'
  }
};

const env = process.env.NODE_ENV || 'development';
const apiConfig = config[env];

Monitoring

Track Key Metrics

class ApiMetrics {
  constructor() {
    this.metrics = {
      ordersReceived: 0,
      ordersAccepted: 0,
      ordersCanceled: 0,
      ordersCompleted: 0,
      apiErrors: 0,
      responseTime: []
    };
  }
  
  recordOrder(status) {
    this.metrics[status]++;
  }
  
  recordResponseTime(duration) {
    this.metrics.responseTime.push(duration);
  }
  
  recordError() {
    this.metrics.apiErrors++;
  }
  
  getStats() {
    return {
      ...this.metrics,
      avgResponseTime: average(this.metrics.responseTime),
      errorRate: this.metrics.apiErrors / this.getTotalRequests()
    };
  }
}

Set Up Alerts

Monitor for:
  • High error rates (> 5%)
  • Slow response times (> 5 seconds)
  • Missing orders (gaps in order IDs)
  • Authentication failures
  • Rate limit hits

Security Checklist

  • API credentials stored in environment variables
  • Credentials encrypted at rest
  • HTTPS only (no HTTP fallback)
  • SSL certificates validated
  • Access tokens never logged
  • Error messages don’t expose sensitive data
  • Request/response logging excludes credentials
  • Regular credential rotation policy
  • Monitoring for unauthorized access
  • Rate limiting handled gracefully

Summary

Do's

  • Store credentials securely
  • Acknowledge orders immediately
  • Use payment_key for API calls
  • Respect rate limits (30s)
  • Cache menu data locally
  • Implement error handling
  • Validate all data
  • Log errors with context
  • Monitor API health
  • Test thoroughly

Don'ts

  • Hard-code credentials
  • Forget to acknowledge orders
  • Use integer ID for operations
  • Poll more than every 30s
  • Fetch menu on every request
  • Ignore error responses
  • Skip data validation
  • Log sensitive data
  • Deploy without monitoring
  • Skip testing

Need Help?

Have questions about best practices? Contact our integration support team.