Field NationDeveloper Platform
Field NationDeveloper Platform
IntroductionQuickstartAPI Playground

Documentation

Webhook EventsPayload StructureDelivery MechanicsWebhook Lifecycle

Support

Migration Guide
Core Concepts

Payload Structure

Understanding webhook payloads - standard fields, work order data, headers, and how to parse webhook requests.


HTTP Request Structure

Field Nation sends webhooks as HTTP POST or PUT requests (configurable) to your endpoint:

POST /webhooks/fieldnation HTTP/1.1
Host: your-endpoint.com
Content-Type: application/json
X-FN-Signature: sha256=bba5b9134ae2fdd020c2b55ab93a113a...
X-FN-Webhook-Id: 892d08af-23bc-44e0-bafa-fac1cc88b559
X-FN-Delivery-Id: 66b2c434-519e-4b3f-be20-00216b600873:1
X-FN-Event-Trace-Id: be107c0c-5095-4d74-aa25-45e450777869
X-FN-Request-Timestamp: 1768920958434

{
  "event": {
    "name": "workorder.status.published",
    "params": {
      "work_order_id": "12345",
      "status_id": 2,
      "previous_status_id": 1
    }
  },
  "timestamp": "2026-01-20T08:55:58-06:00",
  "triggered_by_user": {
    "id": 1068
  },
  "workorder": {
    "id": 12345,
    "title": "Work Order Title",
    "status": { "id": 2, "name": "Published" }
  }
}

Standard Headers

Every webhook request includes these headers:

Prop

Type

Description

X-FN-Signaturestring

HMAC-SHA256 signature for verifying webhook authenticity. Format: `sha256=<hex_digest>`

X-FN-Webhook-Idstring

Unique identifier of the webhook configuration (UUID format)

X-FN-Delivery-Idstring

Unique identifier for this delivery attempt. Format: `uuid:attempt_number`. Use for idempotency

X-FN-Event-Trace-Idstring

Trace ID for correlating this event across systems (UUID format)

X-FN-Request-Timestampstring

Unix timestamp in milliseconds when the webhook was sent

Content-Typestring

Always `application/json`

Custom Headers

You can configure additional headers when creating the webhook:

{
  "webhookAttribute": {
    "header": {
      "Authorization": "Bearer your-api-token",
      "X-Custom-ID": "your-identifier"
    }
  }
}

Reserved Prefix: Headers cannot start with x-fn- as this prefix is reserved for Field Nation system headers.


JSON Payload Structure

Top-Level Fields

Prop

Type

Description

eventobject

Event metadata containing `name` (event type) and `params` (event-specific parameters like status IDs)

timestampstring

ISO 8601 timestamp when the event occurred (includes timezone offset)

triggered_by_userobject

User who triggered this event. Contains `id` field with the user ID

workorderobject

Complete work order object with all current details including status, location, schedule, and custom fields

Example Complete Payload

{
  "event": {
    "name": "workorder.status.published",
    "params": {
      "work_order_id": "12345",
      "work_order_company_id": "100",
      "status_id": 2,
      "previous_status_id": 1,
      "skip_integration": false
    }
  },
  "timestamp": "2026-01-20T08:55:58-06:00",
  "triggered_by_user": {
    "id": 1068
  },
  "workorder": {
    "id": 12345,
    "title": "Router Installation - Site 42",
    "work_order_id": 12345,
    "status": {
      "id": 2,
      "name": "Published"
    },
    "company": {
      "id": 100,
      "name": "Acme Corporation"
    },
    "location": {
      "city": "San Francisco",
      "state": "CA",
      "zip": "94105",
      "country": "US"
    },
    "schedule": {
      "start": { "utc": "2026-01-20T09:00:00Z" },
      "end": { "utc": "2026-01-20T17:00:00Z" }
    },
    "pay": {
      "type": "fixed",
      "base": { "amount": 250.00 }
    },
    "manager": {
      "id": 200,
      "first_name": "John",
      "last_name": "Smith"
    },
    "assignee": null,
    "custom_fields": {
      "results": [
        { "name": "PO Number", "value": "PO-2026-001" }
      ]
    },
    "tags": {
      "results": [
        { "id": 1, "name": "Priority" }
      ]
    },
    "integrations": {
      "results": [
        { "id": "INC0010011", "name": "servicenow v3" }
      ]
    }
  }
}

Work Order Data Object

The data field contains the complete work order object, which matches the structure returned by the REST API.

Key Fields Overview

{
  "id": 12345,
  "title": "Router Installation - Site 42",
  "description": "Install and configure Cisco router",
  "status": "published",
  "type": "installation",
  "priority": "normal",
  "tags": ["priority", "router"],
  "customFields": {
    "ticketNumber": "INC-12345"
  },
  "createdAt": "2025-01-15T09:00:00Z",
  "updatedAt": "2025-01-15T10:30:00Z"
}
{
  "schedule": {
    "serviceWindow": {
      "start": "2025-01-20T09:00:00Z",
      "end": "2025-01-20T17:00:00Z",
      "mode": "hours"
    },
    "estimatedHours": 3
  },
  "location": {
    "address1": "123 Main Street",
    "address2": "Suite 100",
    "city": "San Francisco",
    "state": "CA",
    "zip": "94105",
    "country": "US",
    "coordinates": {
      "latitude": 37.7749,
      "longitude": -122.4194
    },
    "notes": "Enter through main lobby"
  }
}
{
  "pay": {
    "type": "fixed",
    "amount": 250.00,
    "currency": "USD",
    "hourlyRate": null
  },
  "expenses": {
    "mileage": {
      "allowed": true,
      "rate": 0.655,
      "units": "miles"
    },
    "additional": {
      "allowed": true,
      "maxAmount": 50.00
    }
  }
}
{
  "buyer": {
    "id": 456,
    "name": "Acme Corporation",
    "companyId": 789
  },
  "provider": {
    "id": 789,
    "name": "John Smith",
    "userId": 1011,
    "rating": 4.8
  },
  "contacts": [
    {
      "name": "Jane Doe",
      "phone": "+1-555-0123",
      "email": "jane@example.com",
      "role": "site_contact"
    }
  ]
}

Full Schema: The complete work order schema includes 100+ fields. Refer to the REST API documentation for the full specification.


Parsing Webhooks

Essential Parsing Steps

Read Raw Body

Critical for signature verification - read the raw request body before parsing:

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

// Use raw body parser
app.use(express.raw({ type: 'application/json' }));

app.post('/webhooks', (req, res) => {
  const rawBody = req.body; // Buffer
  const payload = JSON.parse(rawBody.toString());

  // Now you can verify signature with rawBody
  // and work with parsed payload
});

Verify Signature

Always verify the x-fn-signature header before processing:

const crypto = require('crypto');

function verifySignature(rawBody, signature, secret) {
  const [algorithm, hash] = signature.split('=');
  const expectedHash = crypto
    .createHmac(algorithm, secret)
    .update(rawBody)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(expectedHash),
    Buffer.from(hash)
  );
}

Extract Event Details

const {
  event,
  timestamp,
  triggered_by_user,
  workorder
} = payload;

// Get delivery ID from headers for idempotency
const deliveryId = req.headers['x-fn-delivery-id'];

// Check idempotency
if (await hasProcessedEvent(deliveryId)) {
  return res.status(200).send('Already processed');
}

Process the Event

// Handle based on event type
switch (event.name) {
  case 'workorder.status.published':
    await handlePublished(workorder);
    break;
  case 'workorder.status.assigned':
    await handleAssigned(workorder);
    break;
  // ... more handlers
}

Respond Quickly

// Acknowledge receipt immediately
res.status(200).send('OK');

// Process asynchronously
processWebhookAsync(payload);

Complete Processing Example

webhook-handler.js
const express = require('express');
const crypto = require('crypto');
const app = express();

// Use raw body for signature verification
app.use(express.raw({ type: 'application/json' }));

app.post('/webhooks/fieldnation', async (req, res) => {
  try {
    // 1. Verify signature
    const signature = req.headers['x-fn-signature'];
    const secret = process.env.WEBHOOK_SECRET;

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

    // 2. Parse payload
    const payload = JSON.parse(req.body.toString());
    const {
      event,
      timestamp,
      triggered_by_user,
      workorder
    } = payload;

    // 3. Get delivery ID for idempotency (from headers)
    const deliveryId = req.headers['x-fn-delivery-id'];

    if (await hasProcessedEvent(deliveryId)) {
      console.log(`Duplicate event: ${deliveryId}`);
      return res.status(200).send('Already processed');
    }

    // 4. Respond immediately (< 5 seconds)
    res.status(200).send('OK');

    // 5. Process asynchronously
    await processEventAsync({
      eventName: event.name,
      eventParams: event.params,
      deliveryId,
      timestamp,
      triggeredByUser: triggered_by_user,
      workorder
    });

  } catch (error) {
    console.error('Webhook processing error:', error);
    res.status(500).send('Internal error');
  }
});

function verifySignature(rawBody, signature, secret) {
  if (!signature) return false;

  const [algorithm, hash] = signature.split('=');
  const expectedHash = crypto
    .createHmac(algorithm, secret)
    .update(rawBody)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(expectedHash),
    Buffer.from(hash)
  );
}

async function hasProcessedEvent(eventId) {
  // Check your database or cache
  return await redis.exists(`event:${eventId}`);
}

async function processEventAsync(event) {
  // Mark as processing
  await redis.setex(`event:${event.deliveryId}`, 86400, 'processed');

  // Handle based on event type
  switch (event.eventName) {
    case 'workorder.status.published':
      await syncToSalesforce(event.workorder);
      await notifyProviders(event.workorder);
      break;

    case 'workorder.status.assigned':
      await updateDispatchBoard(event.workorder);
      await notifyTechnician(event.workorder);
      break;

    case 'workorder.status.work_done':
      await triggerApprovalFlow(event.workorder);
      break;

    case 'workorder.status.approved':
      await generateInvoice(event.workorder);
      await updateAccounting(event.workorder);
      break;
  }
}

app.listen(3000);

Idempotency

Webhooks may be delivered more than once (due to retries). Always check if you've already processed an event.

Idempotency Key: Use the X-FN-Delivery-Id header as your idempotency key. This unique ID is provided with every delivery attempt.

Use Delivery ID for Deduplication

// Redis example
async function processWebhook(req, payload) {
  const deliveryId = req.headers['x-fn-delivery-id'];
  const key = `webhook:${deliveryId}`;

  // Try to set key with NX (only if not exists)
  const wasSet = await redis.set(key, '1', 'EX', 86400, 'NX');

  if (!wasSet) {
    console.log('Duplicate webhook, skipping');
    return;
  }

  // Process the webhook
  await handleEvent(payload);
}

Database-Based Tracking

CREATE TABLE processed_webhooks (
  event_id VARCHAR(255) PRIMARY KEY,
  processed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  event_name VARCHAR(255),
  work_order_id INTEGER
);

CREATE INDEX idx_processed_at ON processed_webhooks(processed_at);
async function hasProcessedEvent(eventId) {
  const result = await db.query(
    'SELECT event_id FROM processed_webhooks WHERE event_id = $1',
    [eventId]
  );
  return result.rows.length > 0;
}

async function markEventProcessed(eventId, eventName, workOrderId) {
  await db.query(
    'INSERT INTO processed_webhooks (event_id, event_name, work_order_id) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING',
    [eventId, eventName, workOrderId]
  );
}

Response Requirements

Your endpoint must respond correctly to receive continued deliveries:

Success Response

HTTP/1.1 200 OK
Content-Type: text/plain

OK

Response Body: Field Nation doesn't parse the response body. Any 2xx status code indicates success.

Response Time

  • < 5 seconds: Acknowledge receipt quickly
  • Long processing: Respond immediately, then process asynchronously
  • Timeout: After 30 seconds, Field Nation considers delivery failed

Error Responses

These status codes trigger retries:

  • 5xx errors: Server errors, will retry
  • 408 Request Timeout: Connection timeout, will retry
  • 429 Too Many Requests: Rate limit, will retry
  • Connection errors: Network failures, will retry

These status codes skip retries:

  • 404 Not Found: Endpoint doesn't exist, no retry
  • 410 Gone: Endpoint permanently removed, no retry

Accessing Full Payload Details

Some work orders have extensive data. The webhook includes the complete current state, but for historical data:

Use REST API for Additional Details

async function enrichWorkOrder(workOrderId) {
  const response = await fetch(
    `https://api.fieldnation.com/api/rest/v2/workorder/${workOrderId}`,
    {
      headers: {
        'Authorization': `Bearer ${accessToken}`
      }
    }
  );

  return await response.json();
}

Access Delivery Log Details

For debugging, retrieve the full request/response:

curl -X GET https://api-sandbox.fndev.net/api/v1/webhooks/delivery-logs/del_xyz789 \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response includes a pre-signed URL to the complete delivery log file.


Best Practices

Parse Once, Validate Early

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

  // Parse once
  const payload = JSON.parse(req.body.toString());

  // Validate required fields
  if (!payload.eventId || !payload.eventName || !payload.data) {
    return res.status(400).send('Invalid payload');
  }

  // Continue processing...
});

Handle Missing Fields Gracefully

Not all work orders include all fields:

const provider = data.provider || null;
const providerName = provider?.name || 'Unassigned';
const tags = data.tags || [];
const customFields = data.customFields || {};

Log Raw Payloads (Development Only)

if (process.env.NODE_ENV === 'development') {
  console.log('Raw webhook payload:', JSON.stringify(payload, null, 2));
}

Security: Never log webhooks in production—they contain sensitive data (PII, financial details, etc.)


Last updated on

Webhook Events

Complete catalog of all 33 webhook events covering the work order lifecycle, status changes, and activity events.

Delivery Mechanics

How webhooks are delivered, retry logic with exponential backoff, message queuing, and automatic deactivation policies.

On this page

HTTP Request Structure
Standard Headers
Custom Headers
JSON Payload Structure
Top-Level Fields
Example Complete Payload
Work Order Data Object
Key Fields Overview
Parsing Webhooks
Essential Parsing Steps
Read Raw Body
Verify Signature
Extract Event Details
Process the Event
Respond Quickly
Complete Processing Example
Idempotency
Use Delivery ID for Deduplication
Database-Based Tracking
Response Requirements
Success Response
Response Time
Error Responses
Accessing Full Payload Details
Use REST API for Additional Details
Access Delivery Log Details
Best Practices
Parse Once, Validate Early
Handle Missing Fields Gracefully
Log Raw Payloads (Development Only)