Create your first webhook and receive Field Nation events in under 15 minutes. Step-by-step guide from setup to testing.
By the end of this quickstart, you'll have:
Estimated Time: 10-15 minutes
You need a publicly accessible HTTPS endpoint to receive webhooks. Choose your approach:
Perfect for testing - No code required!
https://webhook.site/abc123...)Your Webhook URL:
https://webhook.site/abc-1234-def-5678Tip: webhook.site automatically returns 200 OK and displays request details. Perfect for initial testing!
Test with your local server
# macOS (Homebrew)
brew install ngrok/ngrok/ngrok
# Or download from https://ngrok.com/downloadconst express = require('express');
const app = express();
app.use(express.raw({ type: 'application/json' }));
app.post('/webhooks/fieldnation', (req, res) => {
console.log('Webhook received:', req.body.toString());
res.status(200).send('OK');
});
app.listen(3000, () => {
console.log('Webhook server running on port 3000');
});ngrok http 3000Copy the HTTPS forwarding URL:
Forwarding: https://abc123.ngrok-free.app → localhost:3000Your webhook URL: https://abc123.ngrok-free.app/webhooks/fieldnation
For production use
const express = require('express');
const crypto = require('crypto');
const app = express();
// Important: Use raw body for signature verification
app.use(express.raw({ type: 'application/json' }));
app.post('/webhooks/fieldnation', (req, res) => {
// Verify signature (see Security Guide)
const signature = req.headers['x-fn-signature'];
// Process the event
const event = JSON.parse(req.body.toString());
console.log('Event received:', event.eventName);
// Respond quickly (< 5 seconds)
res.status(200).send('OK');
// Process asynchronously
processWebhookAsync(event);
});
function processWebhookAsync(event) {
// Your business logic here
// Update database, trigger workflows, etc.
}
app.listen(443, () => {
console.log('Production webhook server running');
});Deploy to your cloud provider (AWS, GCP, Azure, Heroku, etc.)
You need an OAuth access token to create webhooks via API.
If you don't have them yet:
client_id and client_secretcurl -X POST https://api-sandbox.fndev.net/authentication/api/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET"Response:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600
}Important: Save your access token. It's valid for 1 hour. For production, implement token refresh logic.
Choose your preferred method:
For this quickstart, select:
workorder.createdworkorder.status.publishedworkorder.status.assignedTip: Start with a few events, then expand once you're comfortable.
Note your Webhook ID and Secret - you'll need these for signature verification.
curl -X POST https://api-sandbox.fndev.net/api/v1/webhooks \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-endpoint.com/webhooks",
"method": "post",
"status": "active",
"events": [
"workorder.created",
"workorder.status.published",
"workorder.status.assigned"
],
"notificationEmail": "your-email@example.com"
}'Response:
{
"metadata": {
"timestamp": "2025-01-15T10:30:00Z"
},
"result": {
"id": 123,
"webhookId": "wh_abc123def456",
"url": "https://your-endpoint.com/webhooks",
"method": "post",
"status": "active",
"secret": "01999f51-5c66-4449-b441-6b4a053fee6a",
"events": [
"workorder.created",
"workorder.status.published",
"workorder.status.assigned"
],
"createdAt": "2025-01-15T10:30:00Z"
}
}Save the secret! You'll use this to verify webhook signatures. It's only shown once during creation.
Now let's create an event to trigger your webhook:
Use the Field Nation sandbox UI or API to create a test work order:
Via Sandbox UI:
Via REST API:
curl -X POST https://api-sandbox.fndev.net/api/rest/v2/workorder \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "Test Webhook Work Order",
"description": "Testing webhook delivery",
"schedule": {
"serviceWindow": {
"start": "2025-01-20T09:00:00Z",
"end": "2025-01-20T17:00:00Z"
}
}
}'Within seconds, you should receive a workorder.created event:
{
"event": {
"name": "workorder.created",
"params": {
"work_order_id": "12345",
"work_order_company_id": "100",
"skip_integration": false
}
},
"timestamp": "2026-01-20T10:35:00-06:00",
"triggered_by_user": {
"id": 1068
},
"workorder": {
"id": 12345,
"title": "Test Webhook Work Order",
"status": {
"id": 1,
"name": "Draft"
}
}
}Security critical! Always verify that webhooks actually came from Field Nation:
const crypto = require('crypto');
function verifyWebhookSignature(req, secret) {
// Get signature from header
const signature = req.headers['x-fn-signature'];
if (!signature) return false;
// Parse algorithm and hash
const [algorithm, requestHash] = signature.split('=');
// Compute expected signature
const expectedHash = crypto
.createHmac(algorithm, secret)
.update(req.body) // Raw body buffer
.digest('hex');
// Timing-safe comparison
return crypto.timingSafeEqual(
Buffer.from(expectedHash),
Buffer.from(requestHash)
);
}
// Usage
app.post('/webhooks/fieldnation', (req, res) => {
if (!verifyWebhookSignature(req, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
// Process the verified webhook
const event = JSON.parse(req.body.toString());
console.log('Verified event:', event.eventName);
res.status(200).send('OK');
});import hmac
import hashlib
def verify_webhook_signature(request, secret):
# Get signature from header
signature = request.headers.get('x-fn-signature')
if not signature:
return False
# Parse algorithm and hash
algorithm, request_hash = signature.split('=')
# Compute expected signature
expected_hash = hmac.new(
secret.encode(),
msg=request.body,
digestmod=algorithm
).hexdigest()
# Timing-safe comparison
return hmac.compare_digest(expected_hash, request_hash)
# Usage in Flask
@app.route('/webhooks/fieldnation', methods=['POST'])
def handle_webhook():
if not verify_webhook_signature(request, os.environ['WEBHOOK_SECRET']):
return 'Invalid signature', 401
# Process the verified webhook
event = request.get_json()
print(f"Verified event: {event['eventName']}")
return 'OK', 200<?php
function verifyWebhookSignature($headers, $body, $secret) {
// Get signature from header
if (!isset($headers['x-fn-signature'])) {
return false;
}
// Parse algorithm and hash
[$algorithm, $requestHash] = explode('=', $headers['x-fn-signature']);
// Compute expected signature
$expectedHash = hash_hmac($algorithm, $body, $secret);
// Timing-safe comparison
return hash_equals($expectedHash, $requestHash);
}
// Usage
$headers = getallheaders();
$body = file_get_contents('php://input');
$secret = getenv('WEBHOOK_SECRET');
if (!verifyWebhookSignature($headers, $body, $secret)) {
http_response_code(401);
die('Invalid signature');
}
// Process the verified webhook
$event = json_decode($body);
error_log("Verified event: " . $event->eventName);
http_response_code(200);
echo 'OK';
?>package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
"os"
"strings"
)
func verifyWebhookSignature(r *http.Request, body []byte, secret string) bool {
// Get signature from header
signature := r.Header.Get("x-fn-signature")
if signature == "" {
return false
}
// Parse algorithm and hash
parts := strings.Split(signature, "=")
if len(parts) != 2 {
return false
}
algorithm, requestHash := parts[0], parts[1]
// Compute expected signature (assuming SHA256)
h := hmac.New(sha256.New, []byte(secret))
h.Write(body)
expectedHash := hex.EncodeToString(h.Sum(nil))
// Timing-safe comparison
return hmac.Equal([]byte(expectedHash), []byte(requestHash))
}
func handleWebhook(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
secret := os.Getenv("WEBHOOK_SECRET")
if !verifyWebhookSignature(r, body, secret) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
// Process the verified webhook
// event := parseJSON(body)
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}Critical Security: Always verify signatures in production. Without verification, anyone can send fake webhooks to your endpoint.
Check that your webhook was delivered successfully:
curl -X GET "https://api-sandbox.fndev.net/api/v1/webhooks/delivery-logs?webhookId=YOUR_WEBHOOK_ID" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"Response:
{
"metadata": {
"timestamp": "2025-01-15T10:40:00Z",
"count": 1,
"total": 1
},
"result": [
{
"deliveryId": "del_xyz789",
"webhookId": "wh_abc123",
"eventName": "workorder.created",
"deliveryStatus": 200,
"deliveryAttempt": 1,
"createdAt": "2025-01-15T10:35:05Z"
}
]
}| Issue | Solution |
|---|---|
| 401 Unauthorized | Refresh your OAuth access token |
| Webhook auto-deactivated | Fix endpoint errors, then reactivate |
| Receiving duplicates | Implement idempotency using eventId |
| Signature verification fails | Use raw body buffer, not parsed JSON |
Complete troubleshooting guide →
Last updated on