Field NationDeveloper Platform
Field NationDeveloper Platform
IntroductionQuickstartAPI Playground

Documentation

Support

Migration Guide

Migration Guide

Migrate from legacy webhook implementations to Webhooks v3 with updated event names, improved delivery mechanics, and enhanced features.


What's New in v3

Enhanced Delivery System

  • Redis-based message queuing: Reliable delivery with automatic retries
  • Exponential backoff: Intelligent retry strategy (10s, 20s, 40s, 80s...)
  • Dynamic retry count: Adjusts based on webhook success rate
  • Dead letter queue: Manual retry for permanently failed deliveries

Improved Event System

  • 33 webhook events: Expanded from 15 events in v2
  • Consistent naming: All events follow model.action or model.status.value pattern
  • Better payload structure: Standardized event data format

Security Enhancements

  • HMAC-SHA256 signatures: Cryptographically secure webhook verification
  • IP whitelisting: Restrict access to Field Nation IPs
  • Custom headers: Add authentication tokens to webhook requests

API Improvements

  • Comprehensive delivery logs: Full request/response details for debugging
  • Webhook history: Complete audit trail of all configuration changes
  • Manual retry: Programmatically retry failed deliveries

Breaking Changes

Event Name Changes

Many event names have changed in v3:

v2 Event Namev3 Event NameNotes
work_order.createdworkorder.createdRemoved underscore
work_order.publishedworkorder.status.publishedNow a status change event
work_order.assignedworkorder.status.assignedNow a status change event
work_order.completedworkorder.status.work_doneRenamed to match platform terminology
work_order.approvedworkorder.status.approvedNow a status change event

Action Required: Update your webhook event subscriptions to use new v3 event names.

Payload Structure

v3 introduces a consistent payload structure:

v2 Payload (inconsistent):

{
  "id": "evt_123",
  "event": "work_order.published",
  "work_order_id": 12345,
  "created_at": "2025-01-15T10:00:00Z",
  "work_order": {
    // work order data
  }
}

v3 Payload (standardized):

{
  "eventId": "evt_abc123",
  "eventName": "workorder.status.published",
  "workOrderId": 12345,
  "timestamp": "2025-01-15T10:00:00Z",
  "data": {
    "id": 12345,
    "status": "published",
    // complete work order data
  }
}

Key Changes:

  • id → eventId
  • event → eventName
  • work_order_id → workOrderId
  • created_at → timestamp
  • work_order → data
  • Field names use camelCase instead of snake_case

Signature Verification

v3 uses HMAC-SHA256 instead of basic auth:

v2 (Basic Auth):

// Authorization: Basic base64(username:password)
const auth = req.headers.authorization;
const [username, password] = Buffer.from(auth.split(' ')[1], 'base64')
  .toString()
  .split(':');

v3 (HMAC-SHA256):

// x-fn-signature: sha256=abc123...
const signature = req.headers['x-fn-signature'];
const [algorithm, hash] = signature.split('=');

const expectedHash = crypto
  .createHmac(algorithm, secret)
  .update(rawBody)
  .digest('hex');

const valid = crypto.timingSafeEqual(
  Buffer.from(expectedHash),
  Buffer.from(hash)
);

Complete security guide →


Migration Steps

Audit Current Webhook Usage

Document your current webhooks:

// List all v2 webhooks
const v2Webhooks = await fetch('https://api.fieldnation.com/v2/webhooks', {
  headers: { 'Authorization': 'Bearer YOUR_TOKEN' }
});

// Document subscribed events
const webhooks = await v2Webhooks.json();
webhooks.forEach(webhook => {
  console.log(`Webhook ${webhook.id}:`);
  console.log(`  URL: ${webhook.url}`);
  console.log(`  Events: ${webhook.events.join(', ')}`);
});

Map v2 Events to v3

Create event mapping:

const eventMapping = {
  'work_order.created': 'workorder.created',
  'work_order.published': 'workorder.status.published',
  'work_order.assigned': 'workorder.status.assigned',
  'work_order.completed': 'workorder.status.work_done',
  'work_order.approved': 'workorder.status.approved',
  'work_order.paid': 'workorder.status.paid',
  'work_order.cancelled': 'workorder.status.cancelled',
  // ... map all your events
};

function migrateEvents(v2Events) {
  return v2Events.map(event => {
    if (eventMapping[event]) {
      return eventMapping[event];
    }
    console.warn(`No v3 mapping for event: ${event}`);
    return null;
  }).filter(Boolean);
}

Update Webhook Handler

Modify your handler to support both v2 and v3 payloads during transition:

app.post('/webhooks/fieldnation', express.raw({type: 'application/json'}), (req, res) => {
  // Detect version
  const signature = req.headers['x-fn-signature'];
  const isV3 = !!signature;

  if (isV3) {
    // v3 processing
    if (!verifySignatureV3(req.body, signature, process.env.WEBHOOK_SECRET_V3)) {
      return res.status(401).send('Unauthorized');
    }

    const payload = JSON.parse(req.body.toString());
    processV3Webhook(payload);
  } else {
    // v2 processing (legacy)
    if (!verifyBasicAuth(req.headers.authorization)) {
      return res.status(401).send('Unauthorized');
    }

    const payload = JSON.parse(req.body.toString());
    processV2Webhook(payload);
  }

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

function processV3Webhook(payload) {
  // Handle v3 payload structure
  const { eventId, eventName, workOrderId, data } = payload;
  console.log(`[v3] Event ${eventName} for work order ${workOrderId}`);
  // ... process
}

function processV2Webhook(payload) {
  // Handle v2 payload structure
  const { id, event, work_order_id, work_order } = payload;
  console.log(`[v2] Event ${event} for work order ${work_order_id}`);
  // ... process
}

Create v3 Webhooks

Create new v3 webhooks in sandbox:

async function createV3Webhook(v2Webhook) {
  // Map v2 events to v3
  const v3Events = migrateEvents(v2Webhook.events);

  // Create v3 webhook
  const response = await fetch(
    'https://api-sandbox.fndev.net/api/v1/webhooks',
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${accessToken}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        url: v2Webhook.url,
        method: 'post',
        status: 'active',
        events: v3Events,
        notificationEmail: 'alerts@example.com'
      })
    }
  );

  const v3Webhook = await response.json();

  console.log(`Created v3 webhook ${v3Webhook.result.webhookId}`);
  console.log(`Secret: ${v3Webhook.result.secret}`);

  return v3Webhook.result;
}

Test in Sandbox

Thoroughly test v3 webhooks before production:

# Use ngrok for local testing
ngrok http 3000

# Create test webhook
curl -X POST https://api-sandbox.fndev.net/api/v1/webhooks \
  -H "Authorization: Bearer YOUR_SANDBOX_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-ngrok-url.ngrok-free.app/webhooks",
    "method": "post",
    "status": "active",
    "events": ["workorder.status.published", "workorder.status.assigned"]
  }'

# Trigger test events
# ... create and publish work orders in sandbox

Parallel Run

Run v2 and v3 webhooks in parallel:

// Keep v2 webhook active
// Add v3 webhook pointing to same endpoint
// Your handler supports both versions

app.post('/webhooks', (req, res) => {
  const signature = req.headers['x-fn-signature'];
  const version = signature ? 'v3' : 'v2';

  console.log(`Received ${version} webhook`);

  // Process based on version
  if (version === 'v3') {
    processV3(req);
  } else {
    processV2(req);
  }

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

Monitor Both Versions

Track v3 delivery health:

async function compareVersions() {
  // v2 webhooks (your monitoring)
  const v2Stats = await getV2Stats();

  // v3 webhooks (delivery logs API)
  const v3Logs = await fetch(
    'https://api-sandbox.fndev.net/api/v1/webhooks/delivery-logs?webhookId=wh_abc123',
    {
      headers: { 'Authorization': `Bearer ${token}` }
    }
  );

  const v3Stats = calculateStats(await v3Logs.json());

  console.log('v2 Success Rate:', v2Stats.successRate);
  console.log('v3 Success Rate:', v3Stats.successRate);

  // Continue if v3 performs as well as v2
  return v3Stats.successRate >= v2Stats.successRate;
}

Cutover to v3

Once confident, switch to v3 only:

// 1. Update handler to v3 only
app.post('/webhooks', express.raw({type: 'application/json'}), (req, res) => {
  const signature = req.headers['x-fn-signature'];

  if (!verifySignatureV3(req.body, signature, secret)) {
    return res.status(401).send('Unauthorized');
  }

  const payload = JSON.parse(req.body.toString());
  processV3Webhook(payload);

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

// 2. Deactivate v2 webhooks
await deactivateV2Webhooks();

// 3. Monitor for issues
await monitorV3Health(24); // 24 hours

Cleanup

After successful migration:

// Delete v2 webhooks
await deleteV2Webhooks();

// Remove v2 compatibility code
// Update documentation

Feature Adoption

Use New v3 Events

Subscribe to new events not available in v2:

const newV3Events = [
  'workorder.routed',
  'workorder.requested',
  'workorder.declined',
  'workorder.undeclined',
  'workorder.task_completed',
  'workorder.task_incomplete',
  'workorder.provider_upload',
  'workorder.message_posted',
  'workorder.custom_field_value_updated',
  'workorder.schedule_updated',
  'workorder.tag_added',
  'workorder.tag_removed',
  'workorder.part_updated'
];

Implement Retry Logic

Leverage v3's automatic retry system:

// Monitor delivery logs
async function monitorDeliveries(webhookId) {
  const logs = await fetch(
    `https://api-sandbox.fndev.net/api/v1/webhooks/delivery-logs?webhookId=${webhookId}`,
    {
      headers: { 'Authorization': `Bearer ${token}` }
    }
  );

  const { result } = await logs.json();

  const failed = result.filter(log => log.deliveryStatus >= 400);

  if (failed.length > 0) {
    console.log(`${failed.length} failed deliveries`);

    // Manual retry if needed
    for (const log of failed) {
      await retryDelivery(log.deliveryId);
    }
  }
}

Rollback Plan

If issues arise, you can rollback:

// 1. Reactivate v2 webhooks
await reactivateV2Webhooks();

// 2. Deactivate v3 webhooks
await fetch(`https://api-sandbox.fndev.net/api/v1/webhooks/${webhookId}`, {
  method: 'PUT',
  headers: {
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ status: 'inactive' })
});

// 3. Restore v2 handler logic

Need Help?

  • Support Portal: app.fieldnation.com/support-cases
  • API Documentation: developers.fieldnation.com/docs/webhooks

Last updated on

Debugging Webhooks

Advanced debugging techniques, tools, and strategies for troubleshooting complex webhook integration issues.

On this page

What's New in v3
Enhanced Delivery System
Improved Event System
Security Enhancements
API Improvements
Breaking Changes
Event Name Changes
Payload Structure
Signature Verification
Migration Steps
Audit Current Webhook Usage
Map v2 Events to v3
Update Webhook Handler
Create v3 Webhooks
Test in Sandbox
Parallel Run
Monitor Both Versions
Cutover to v3
Cleanup
Feature Adoption
Use New v3 Events
Implement Retry Logic
Rollback Plan
Need Help?