# Field Nation Integrations - Full Documentation > Complete documentation for the Field Nation platform. Contains the full text of all REST API guides, Webhooks documentation, pre-built connector guides, and integration resources. ## Table of Contents - Getting Started (4) - API Overview (44) - Webhooks Overview (22) - Pre built Connectors (42) - Resources (5) - REST Playground (156) - Webhooks Playground (12) ## Getting Started (4) ### Choosing your approach URL: /docs/getting-started/choosing-your-approach Field Nation offers multiple integration paths that work together. The right starting point depends on what system you are connecting, how much control you need, and whether you need real-time event notifications. ## The Decision Path Start from the top and follow the path that matches your situation. ```mermaid flowchart TD Q1{"Connector exists\nfor your platform?"} Q2{"System has an\nOpenAPI spec?"} Q3{"Need more\ncontrol?"} Q4{"Need more\ncontrol?"} R1["Pre-built Connector"] R2["REST Connector"] R3["REST API + Webhooks"] OK(["You're all set"]) Q1 -- "Yes" --> R1 Q1 -- "No" --> Q2 Q2 -- "Yes" --> R2 Q2 -- "No" --> R3 R1 --> Q3 Q3 -- "No" --> OK Q3 -- "Yes" --> R3 R2 --> Q4 Q4 -- "No" --> OK Q4 -- "Yes" --> R3 ``` > [INFO] **Where do webhooks fit?** Webhooks are not a separate integration path — they are the **real-time event layer**. Pre-built connectors and the REST Connector have event sync built-in. If you use the REST API, pair it with Webhooks so you get notified when work orders change instead of polling. --- ## Four Capabilities Compared | | Pre-built Connector | REST Connector | REST API | Webhooks | |---|---|---|---|---| | **Purpose** | Sync with supported platforms | Sync with any OpenAPI system | Full programmatic access | Real-time event notifications | | **Best for** | Salesforce, ServiceNow, Autotask, ConnectWise, etc. | Niche or internal systems with an OpenAPI spec | Custom integrations needing full control | Knowing when something changed without polling | | **Technical skill** | No code / low code | Low code | Developer required | Developer required | | **Setup time** | Hours | Hours to days | Days to weeks | Minutes (once API is set up) | | **Data flow control** | Field mappings via UI | Field mappings via UI | Complete programmatic control | Event-driven push (you choose which events) | | **Real-time events** | Built-in | Built-in | Pair with Webhooks | This IS the event layer | | **Maintenance** | Managed by Field Nation | Managed by Field Nation | Your team | Your team (endpoint + signature verification) | --- ## Path 1: Pre-built Connector **Start here if a connector exists for your system.** Pre-built connectors are no-code integrations maintained by Field Nation. They handle authentication, field mapping, bi-directional sync, and real-time event triggers through a configuration UI. **Event handling:** Connectors automatically trigger sync when work orders are created, updated, or change status. You do not need to set up webhooks separately. > [INFO] **When to escalate** Some buyers start with a connector and later add REST API calls for capabilities that connector field mappings do not cover — bulk operations, custom reporting, or advanced workflow control. The two approaches work together. --- ## Path 2: REST Connector **Try this if your system is not in the list above but provides an OpenAPI specification.** The REST Connector is a universal, configuration-driven connector. Upload your system's OpenAPI spec and Field Nation auto-discovers available endpoints. Map fields through the same UI as pre-built connectors. ### Upload your OpenAPI spec Provide your system's OpenAPI 3.x specification file. The REST Connector parses it and discovers available endpoints, request schemas, and authentication methods. ### Configure field mappings Map Field Nation work order fields to your system's fields through the visual mapper. Set up inbound and outbound sync directions. ### Enable event triggers Configure which events (work order created, status changed, completed) trigger sync between systems. Event handling is built-in — no separate webhook setup needed. **Good fit for:** Internal tools, niche platforms, or any system that exposes a REST API with an OpenAPI spec file. [Learn about the REST Connector](/docs/connectors/platforms/rest-connector/overview) --- ## Path 3: REST API + Webhooks **Use this when you need full control over data flow.** The REST API gives you direct, programmatic access to every Field Nation capability. Pair it with Webhooks for real-time event notifications. ### Why pair them? Without webhooks, your integration must poll the REST API repeatedly to check for changes — wasteful and slow. With webhooks, Field Nation pushes events to your endpoint the moment something happens. | Pattern | Without Webhooks | With Webhooks | |---|---|---| | **Detect status change** | Poll GET /workorders every N minutes | Receive instant HTTP POST when status changes | | **Latency** | Minutes (depends on poll interval) | Seconds | | **API calls** | Hundreds per hour | Only when you need to act on an event | | **Complexity** | Simpler to start | Requires endpoint + signature verification | ### Common reasons to choose this path - Your system does not have a connector or OpenAPI spec - You need bulk operations, custom reporting, or advanced workflow logic - You need control over exactly when and how data moves - You started with a connector but hit a limitation in field mappings --- ## How the Pieces Fit Together The four capabilities are layers, not alternatives: ```mermaid graph TB subgraph "Real-time Events" W["Webhooks\n33 event types"] end subgraph "Data Access" R["REST API\nFull programmatic control"] end subgraph "No-Code Integration" C["Pre-built Connectors\n8 platforms"] RC["REST Connector\nAny OpenAPI system"] end W -.->|"Built into"| C W -.->|"Built into"| RC W -->|"Pair with"| R C -->|"Escalate to"| R RC -->|"Escalate to"| R ``` **Typical integration structures:** Pre-built connector handles standard work order sync with Salesforce. REST API runs a nightly script for custom reporting across all work orders. REST API applies conditional routing logic that connector mappings cannot express. REST API creates and manages work orders programmatically. Webhooks push status updates to your internal dashboard in real-time. Your code handles retry logic, deduplication, and business rules. Connector handles the standard field sync baseline. REST API handles bulk imports and custom financial operations. Webhooks push critical alerts (work order completed, SLA approaching) to your monitoring system independently. --- ## Next Steps --- ### Platform overview URL: /docs/getting-started/platform-overview ## The Mental Model Before you write code, it is critical to understand **Architecture (Space)** and **Lifecycle (Time)**. Field Nation is a marketplace state machine. Your integration essentially moves a Work Order through specific states. ### 1. The Architecture (Space) How does your system connect to ours? There are two primary paths: | Layer | Description | | :--- | :--- | | **Broker Layer** | The "Low Code" path. Middleware that translates your system's data (Salesforce, ServiceNow) into Field Nation work orders automatically. | | **Client API** | The "Full Control" path. Your custom code speaks directly to our REST API. You handle auth, retries, and logic. | | **FN Core** | The heart of the platform where Work Orders, Providers, and Financials live. | --- ### 2. The Lifecycle (Time) The **Work Order** is the atomic unit of Field Nation. It flows through a strict state machine. Your integration's job is to trigger these transitions or react to them. ![Work Order Lifecycle](./images/wo_lifecycle.webp) ### Draft Draft work orders are not visible to providers in the marketplace. You can fully edit and change all work order details while in this status. ### Published / Routed **Publishing** a work order makes it visible in the marketplace, allowing providers to review the details and request the work. You can review the profiles of the requesting providers and assign the best-qualified technician. **Routing** a work order sends it directly to specific provider(s) of your choosing. The routed providers can review the work details and accept the work. The first provider to accept the route is automatically assigned. ### Assigned Once a provider is assigned (either by accepting a route or being selected from requests), the work order moves to the assigned state. A binding contract is formed. Upon the scheduled start time, the provider will travel to the site and begin performing the work. Once all requirements and tasks are finished, the provider will mark the work order as complete. ### Done Work orders in the done state are indicating the technician has completed the work and the deliverables are ready for your review. Approve the work order if you are satisfied with the work completed and the provided documentation. ### Approved Payment processing for Approved work orders happens automatically (typically once a week on Friday), based on the payment terms listed in the work order contract. ### In-flight & Issue While not strict linear statuses, it is important to monitor work orders that are **In-flight** (scheduled for today or tomorrow) or have an **Issue** (a problem reported by the provider on-site). API integrations often hook into these events to alert dispatchers. --- ## Core Concepts The pieces on the board. ### The Users **That's you.** The company posting work. You define the *What* (Task), *Where* (Location), and *How Much* (Pay). The independent technician. They have **Skills**, **Ratings**, and **Equipment**. They are not employees; they are marketplace participants. ### The Work A single job at a specific location. Contains: * **Schedule**: When it must be done. * **Pay**: Fixed price or Hourly rate. * **Tasks**: Checklist of required actions. A container for grouping 100s of similar Work Orders. Useful for "Rollouts" (e.g., "Upgrade POS at 500 stores"). ### The Mechanics The allowed time range. Can be **Fixed** (Start at 9:00 AM) or **Flexible** (Between 9:00 AM and 5:00 PM). A saved list of your "Favorite" providers. Use this to route work to people you trust first. --- ### Prerequisites URL: /docs/getting-started/prerequisites ## Checklist Ensure you have the following before starting development: 1. **Buyer Account**: Application access to post and manage work. 2. **Integration Contract**: Authorization to use the API. 3. **Sandbox Credentials**: `client_id` and `client_secret` for authentication. 4. **Company ID**: Your unique identifier for API requests. --- ## 1. Access & Contract You must have an executed **Integration Contract** and an active **Buyer Account**. > [INFO] **Don't have an account or contract?** [Contact Sales](https://fieldnation.com/contact) or your Account Manager. ## 2. API Credentials To get your `client_id` and `client_secret`: ### Log in Log in to your [Field Nation](https://app.fieldnation.com) account. ### Navigate to Integration Navigate to **Integration** → **Field Services** from the top navigation. ### Open REST API settings Click **Manage** on the REST API v3 card. ### Copy credentials Copy the **Client ID** & **Client Secret**. *You can generate a new secret by clicking the "Generate new secret" button.* ![API Secrets](./images/secrets.webp) > **Important** Store your `client_id` and `client_secret` securely. Do not commit them to version control. ## 3. Company ID You will need your Field Nation **Company ID** for API requests (`x-fn-company-id` header) and support cases. ### Log in Log in to the [Field Nation Platform](https://app.fieldnation.com). ### Open Company Settings Click your user icon (top right) → **Company Settings**. ### Find your Company ID Click **Company Profile**. Your ID is the number at the end of the URL. ![Company Profile](./images/company_profile.webp) --- ## Technical Requirements ### Network **Outbound Access (HTTPS / 443):** * **Sandbox**: `https://api-sandbox.fndev.net` * **Production**: `https://api.fieldnation.com` *IP Whitelisting is not required for outbound calls.* **Inbound Access (Allowlist):** Ensure your firewall allows Field Nation's IPs: **Sandbox:** ```text 44.225.211.232 44.237.253.26 ``` **Production:** ```text 3.226.5.230 34.198.172.230 ``` ### Tools * **Authentication**: OAuth 2.0 (Client Credentials Grant). * **Payloads**: JSON (up to 5MB). * **Security**: HTTPS required for all endpoints. ## Ready? Once you have your credentials and Company ID, go to the [Quick Start](/docs/getting-started/quick-start) to connect. --- ### Quick start URL: /docs/getting-started/quick-start Welcome to the Field Nation Integration Developer Portal. This documentation provides you everything you need to integrate your system with Field Nation's platform. ## Prerequisites - Active Buyer Account - Integration contract in place - Sandbox credentials received - Development environment setup - Company ID documented > [INFO] **Don't have access to integrate yet?** Check out our website to get in touch with a member of our team. [Field Nation Integrations](https://fieldnation.com/field-nation-integrations) ## Quick Connect Get your first API token and create a work order in seconds. ### Authenticate Generate an OAuth 2.0 token to verify your access with sandbox. ```bash curl -X POST "https://api-sandbox.fndev.net/authentication/api/oauth/token" \ -H "Content-Type: application/json" \ -d '{ "grant_type": "password", "client_id": "YOUR_CLIENT_ID", "client_secret": "YOUR_CLIENT_SECRET", "username": "YOUR_USERNAME", "password": "YOUR_PASSWORD" }' ``` 1. Open Postman. 2. Create a **POST** request to `https://api-sandbox.fndev.net/authentication/api/oauth/token`. 3. Set **Body** type to `JSON`. 4. Add the payload: ```json { "grant_type": "password", "client_id": "YOUR_CLIENT_ID", "client_secret": "YOUR_CLIENT_SECRET", "username": "YOUR_USERNAME", "password": "YOUR_PASSWORD" } ``` > [INFO] **Get Credentials**: Your `client_id` and `client_secret` are provided by the implementation team during onboarding. See [Prerequisites](/docs/getting-started/prerequisites) for details on credentials and Company IDs. ### Create Work Order Post a basic work order to the sandbox environment. ```bash curl -X POST "https://api-sandbox.fndev.net/api/rest/v2/workorders?access_token=YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "title": "Install Point of Sale", "description": { "html": "

Swap out the defective terminal.

" }, "types_of_work": [{ "id": 62, "isPrimary": true }], "location": { "mode": "custom", "address1": "123 Main St", "city": "Minneapolis", "state": "MN", "zip": "55401", "country": "US" }, "schedule": { "service_window": { "mode": "exact", "start": { "utc": "2026-07-15 09:00:00" } } }, "pay": { "type": "fixed", "base": { "amount": 150, "units": 1 } } }' ``` ## API Quick Links Tools to help you build and test faster. ## Capabilities Choose the right tool for your integration. --- ## What is Field Nation? Field Nation connects you with skilled technicians to get work done. - **Automate** work order creation from your system. - **Track** real-time updates from the field. - **Manage** assignments and routing programmatically. - **Sync** deliverables and photos back to your records. - **Scale** operations without increased overhead. --- ## API Overview (44) ### Api playground URL: /docs/rest-api/api-playground Visit the REST API v2 playground to try requests, view examples, and inspect responses: --- ### Introduction URL: /docs/rest-api/introduction The Field Nation Client API allows you to integrate your systems directly with the Field Nation marketplace. You can fully automate the lifecycle of work orders, from creation and assignment to approval and payment. ## Base URLs All API requests are made to the following base URLs. Note that we recommend developing and testing your integration in the **Sandbox** environment before going live. | Environment | Base URL | Description | | :--- | :--- | :--- | | **Production** | `https://api.fieldnation.com/api/rest/v2` | Live environment. Real providers, real money. | | **Sandbox** | `https://api-sandbox.fndev.net/api/rest/v2` | Testing environment. Safe for development. | > **Note**: The Sandbox environment is completely separate from Production. credentials and data do not share across environments. ## Key Capabilities Securely connect using OAuth 2.0 (Client Credentials or User based). Understand the flow of a work order from Draft to Paid. Learn how to post jobs to the marketplace. Route, assign, and manage active work. ## Support If you need assistance with your integration, please contact our implementation support team or refer to the [Field Nation Support Center](https://support.fieldnation.com). --- ### Quick start URL: /docs/rest-api/quick-start ## Quick Start Guide This guide will take you from **Zero** to **Published Work Order** in three steps. ### Prerequisites - **API Credentials**: You need a `client_id`, `client_secret`, `username`, and `password`. - **Environment**: We will use the **Sandbox** (`https://api-sandbox.fndev.net/api/rest/v2`) for this guide. --- ### Account and Authentication First, exchange your credentials for an access token. ```bash curl -X POST "https://api-sandbox.fndev.net/authentication/api/oauth/token" \ -H "Content-Type: application/json" \ -d '{ "grant_type": "password", "client_id": "YOUR_ID", "client_secret": "YOUR_SECRET", "username": "YOUR_USER", "password": "YOUR_PASSWORD" }' ``` ```json { "access_token": "eyJ0eXAi...", "expires_in": 3600 } ``` ### Create a Draft Work Order Create a simple job. We will set it to "Draft" mode so we can review it. > [INFO] Use `type_of_work: { id: 62 }` (Point of Sale) for this test. ```bash curl -X POST "https://api-sandbox.fndev.net/api/rest/v2/workorders?access_token=YOUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "title": "Quick Start Test Job", "description": { "html": "

This is a test.

" }, "type_of_work": { "id": 62 }, "location": { "mode": "custom", "address1": "123 Test St", "city": "Minneapolis", "state": "MN", "zip": "55401", "country": "US" }, "pay": { "type": "fixed", "base": { "amount": 100, "units": 1 } }, "schedule": { "service_window": { "mode": "exact", "start": { "utc": "2026-07-01 09:00:00" } } } }' ``` ```json { "id": 998877, "status": { "id": 1, "name": "Draft" } } ``` ### Publish to Marketplace Now that it exists, make it live so providers can see it. ```bash curl -X POST "https://api-sandbox.fndev.net/api/rest/v2/workorders/998877/publish?access_token=YOUR_TOKEN" ``` **Success!** You have just dispatched your first work order programmatically. --- ## Next Steps Now that you've made your first call, dive deeper into the core systems: - [**Work Order Lifecycle**](/docs/rest-api/core-concepts/wo-lifecycle): Understand the states (Route, Assign, Work Done). - [**Create Work Order**](/docs/rest-api/work-orders/basics/create): Learn about Templates, Bundles, and Custom Fields. - [**Webhooks**](/docs/webhooks/introduction): Set up real-time event listening. --- ### Taxonomy URL: /docs/rest-api/core-concepts/taxonomy # Classification & Taxonomy Correctly classifying your work is essential for finding the right talent. Field Nation uses specific structures to categorize jobs. ## Types of Work **Types of Work** are the primary way to categorize a job (e.g., "Cabling", "Point of Sale", "Networking"). * **Matching**: Providers list these types of work in their profiles. * **Usage**: When creating a work order, you usually specify a `type_of_work` ID. * **Deprecated**: Note that older fields like `service_type` are often replaced by `type_of_work`. ### Listing Types of Work Retrieve a list of all available job categories. `GET /types-of-work` > Always ensure you are using a valid `type_of_work` ID that exists in the system. You can retrieve valuable IDs via the lookup endpoints. ## Templates **Projects** and **Templates** act as blueprints for your work orders. ### Work Order Templates A template pre-fills many fields of a work order, such as: * Description / Scope of Work * Pay Rates * Select Types of Work * Custom Field requirements **Why use them?** Using templates (`template_id` in your Create request) significantly reduces the size of your API payload and ensures consistency across thousands of work orders. ### Listing Templates Retrieve available templates to use in your work order creation. `GET /templates` ### Projects Projects are high-level containers for work orders. They are useful for: * Reporting (Grouping costs by project). * Access Control (Granting managers access to specific projects). * Default configurations. --- ### Wo lifecycle URL: /docs/rest-api/core-concepts/wo-lifecycle # The Work Order Lifecycle The lifecycle of a work order is the central concept in the Field Nation platform. Understanding these states is critical for building a robust integration. ## The Flow ### 1. Draft When you first `POST` a work order, it is created in the **Draft** state. * **Visibility**: Only visible to you (the buyer). * **Actions**: You can edit all details. * **Next Step**: [Publish](/docs/rest-api/work-orders/workflow/lifecycle#1-publish) the work order to make it live. ### 2. Published (Routed) Once published, the work order enters the marketplace. * **Visibility**: Visible to providers you have routed it to (or the general marketplace if open). * **Status**: `Routed` or `Published`. * **Actions**: Providers can view requests and submit counter-offers. * **Next Step**: [Assign](/docs/rest-api/work-orders/workflow/assignments) a provider. ### 3. Assigned You have selected a provider and they have accepted the work. * **Status**: `Assigned`. * **Actions**: The provider checks in, performs work, and checks out. ### 4. Work Done (Completed) The provider has marked the work as complete. * **Status**: `Work Done`. * **Actions**: You must review the work deliverables (photos, notes). * **Options**: * **Approve**: Validated work. Moves to payment. * **Incomplete**: Reject work. Sends back to provider for fixes. ### 5. Approved The work is accepted. * **Status**: `Approved`. * **Actions**: The system calculates final pay (including bonuses/expenses) and initiates the payment process. ### 6. Paid Payment has been transferred to the provider. * **Status**: `Paid`. * **Note**: This is the final terminal state. ## State Diagram ```mermaid graph TD Draft -->|Publish| Routed Routed -->|Assign| Assigned Assigned -->|Provider Completes| WorkDone WorkDone -->|Approve| Approved WorkDone -->|Incomplete| Assigned Approved -->|System Process| Paid Routed -->|Revert| Draft ``` > [INFO] **Integration Tip**: Most integrations poll for status changes or listen to **Webhooks** to trigger their internal workflows (e.g., closing a ticket when the work order is `Approved`). ## Related - [Workflow Management](/docs/rest-api/work-orders/workflow/lifecycle) - API endpoints for each state - [Assignments](/docs/rest-api/work-orders/workflow/assignments) - Managing provider assignments - [Taxonomy](/docs/rest-api/core-concepts/taxonomy) - Understanding work order structure --- ### Authentication URL: /docs/rest-api/getting-started/authentication # Authentication The Field Nation API uses **OAuth 2.0** for authentication. You must exchange your credentials for an access token, which is then included in the header of every API request. ## OAuth Flow ### Get Credentials Obtain your `client_id`, `client_secret`, `username`, and `password` from your Field Nation representative or integration settings. ### Request Access Token Make a `POST` request to the token endpoint to exchange your credentials for a bearer token. ```bash curl -X POST "https://api.fieldnation.com/authentication/api/oauth/token" \ -H "Content-Type: application/json" \ -d '{ "grant_type": "password", "client_id": "YOUR_CLIENT_ID", "client_secret": "YOUR_CLIENT_SECRET", "username": "YOUR_USERNAME", "password": "YOUR_PASSWORD" }' ``` ```json { "access_token": "eyJ0eXAiOiJKV1QiLCJhbG...", "expires_in": 3600, "token_type": "Bearer", "scope": "basic", "refresh_token": "cf83e1357eefb8bdf1542850d66d8007" } ``` ### Use the Token Append the `access_token` as a query parameter on your API requests. ```bash curl -X GET "https://api.fieldnation.com/api/rest/v2/workorders/12345?access_token=eyJ0eXAiOiJKV1QiLCJhbG..." ``` > [INFO] **Token Expiration**: Access tokens are valid for a limited time (usually 1 hour). When a token expires, use the `refresh_token` or re-authenticate to get a new one. ## Errors Common authentication errors you might encounter: | Status Code | Message | Description | | :--- | :--- | :--- | | `401` | Unauthorized | The token is missing, invalid, or expired. | | `403` | Forbidden | The token is valid, but the user does not have permission for this resource. | --- ### Basics URL: /docs/rest-api/getting-started/basics # API Basics Understanding these core concepts will help you integrate more effectively and avoid common pitfalls. ## Pagination Most list endpoints (like `GET /workorders`) are paginated to improve performance. The API standardizes pagination using `page` and `per_page` query parameters. | Parameter | Type | Default | Description | | :--- | :--- | :--- | :--- | | `page` | integer | `1` | The page number to retrieve. | | `per_page` | integer | `25` | Number of items per page. | > [INFO] The maximum value for `per_page` varies by endpoint. Check the response `metadata` for the actual page size returned. ### Response Metadata Paginated responses include a `metadata` object with counts and active state: ```json { "metadata": { "total": 150, "per_page": 25, "pages": 6, "page": 1, "sort": "schedule", "order": "asc", "list": "workorders_all" }, "results": [ ... ] } ``` ## Filtering & Sorting The API supports powerful filtering and sorting capabilities to help you find the exact data you need. ### Filtering (`f_`) Filters are passed as query parameters prefixed with `f_`. For `GET /workorders`, always include a `list` parameter to scope results to a lifecycle stage, and pass `sticky=false` to keep each request self-contained. * `f_state`: Filter by state abbreviation (e.g., `TX`, `CA`). * `f_project`: Filter by project ID. * `f_created_date`: Filter by creation date or range (e.g., `2026-01-01` or `2026-01-01,2026-03-31`). **Example**: `GET /workorders?list=workorders_all&f_project=55&f_state=TX&sticky=false` For the full list of available filters, see [Search & Filter Work Orders](/docs/rest-api/work-orders/basics/search). ### Sorting Use `sort` and `order` to control result ordering. * `sort`: The field to sort on. Valid values: `id`, `title`, `schedule`, `status`, `pay`, `created_date`. * `order`: `asc` or `desc`. **Example**: `GET /workorders?list=workorders_all&sort=created_date&order=desc&sticky=false` ## Content Types * **Requests**: Must use `Content-Type: application/json` (unless uploading files). * **Responses**: All responses are returned as `application/json`. ## Date & Time Field Nation uses a specific object structure for timestamps to explicitly handle Timezones. **You must typically send dates in this format.** ```json "start": { "utc": "2023-11-07 13:00:00" } ``` * **Format**: `YYYY-MM-DD HH:mm:ss` (24-hour format). * **Timezone**: Always convert to **UTC** before sending. The API handles the conversion to local time based on the work order's location. ## Rate Limiting To ensure fair usage and stability, the API enforces rate limits. If you exceed the limit, you will receive a `429 Too Many Requests` response. * **Best Practice**: Implement exponential backoff when you encounter a `429` or `5xx` error. * **Webhooks**: Prefer using Webhooks for real-time updates instead of polling the API, to save your rate limit quota. ## Error Handling Standard HTTP status codes are used to indicate success or failure. For detailed resolution steps, see the [Troubleshooting](/docs/rest-api/getting-started/troubleshooting) guide. | Code | Meaning | | :--- | :--- | | `200` | **OK**. The request was successful. | | `201` | **Created**. A resource was successfully created. | | `400` | **Bad Request**. Invalid payload. | | `401` | **Unauthorized**. Authentication failed. | | `403` | **Forbidden**. Permission denied. | | `404` | **Not Found**. Resource does not exist. | | `500` | **Server Error**. Internal error. | ## Troubleshooting If you encounter issues, check these common pitfalls: * **Check JSON Syntax**: Ensure your payload is valid JSON. * **Validate IDs**: key IDs like `type_of_work`, `project_id`, or `template_id` must valid in the environment you are using (Sandbox IDs != Production IDs). * **Date Format**: Ensure all dates are in the `{ "utc": "..." }` format. * **Token Expiration**: Your token may have expired. Refresh it. * **Environment Mismatch**: Are you using a Sandbox token against Production endpoints? * **Scope**: Your API user may lack the specific permission (e.g., "Financials") to access this resource. Contact your admin. --- ### Troubleshooting URL: /docs/rest-api/getting-started/troubleshooting # Troubleshooting If you run into issues integrating with the Field Nation API, use this guide to diagnose and resolve common errors. ## Standard Error Codes | Code | Meaning | Action | | :--- | :--- | :--- | | `400` | **Bad Request** | Check your JSON syntax and data types. | | `401` | **Unauthorized** | Check your token validity. | | `403` | **Forbidden** | Check your user permissions (Scopes). | | `404` | **Not Found** | Validate your IDs. | | `429` | **Too Many Requests** | Slow down. Implement exponential backoff. | | `500` | **Server Error** | Retry once. If it persists, contact support. | ## Common Resolution Guides **Symptom**: You send a date, but get a validation error. **Fix**: Ensure you are using the precise `{ "utc": "YYYY-MM-DD HH:mm:ss" }` object structure. Plain strings like `"2023-01-01"` are often rejected in strict fields. **Symptom**: You are testing in Sandbox using IDs (Type of Work, Template, Project) from Production. **Fix**: IDs are unique to each environment. You must recreate your setup (Projects, Templates) in Sandbox and use *those* new IDs for testing. **Symptom**: Used to work, now returns 401. **Fix**: Access tokens expire after 1 hour. Your code must handle the 401 response by using the `refresh_token` to fetch a fresh `access_token` automatically. **Symptom**: You have a valid token, but cannot modify Financials or Users. **Fix**: The API user associated with your credentials likely lacks the specific permission bit (e.g., "Manage Providers" or "View Financials"). Log in to the UI with that user to verify access, or ask an Admin to grant the permission. --- ### Clients URL: /docs/rest-api/resources/clients # Clients **Clients** represent your end-customers (e.g., "Retail Chain X", "Enterprise Corp"). Tag work orders with a `client_id` to track work and costs by customer. ## List Clients Retrieve a paginated list of client companies. `GET /clients` **Query Parameters:** | Parameter | Description | |-----------|-------------| | `columns` | Fields to include in response | | `per_page` | Results per page | | `f_deactivated` | Filter by deactivated status (0 or 1) | | `f_deleted` | Filter by deleted status (0 or 1) | | `list` | Saved list/filter name | | `smart_list` | Sort by recently used/created | ### Response ```json { "results": [ { "id": 102, "name": "Retail Chain X", "active": true, "created": "2026-01-15T10:30:00Z", "work_orders_count": 156 } ], "metadata": { "total": 25, "page": 1, "per_page": 20 } } ``` ## Create Client `POST /clients` ```json { "name": "New Customer Inc", "notes": "VIP customer - priority support" } ``` ## Get Client Details `GET /clients/{client_id}` ## Using Clients in Work Orders When creating a work order, associate it with a client: ```json { "title": "Install POS System", "client": { "id": 102 } } ``` > **Reporting**: Clients enable cost rollup and reporting. All work orders tagged with a client can be filtered and analyzed together. ## Related Resources - [Projects](/docs/rest-api/resources/projects) - Initiatives within a client - [Work Order Creation](/docs/rest-api/work-orders/basics/create) - Using clients when creating work orders --- ### Company URL: /docs/rest-api/resources/company # Company The Company endpoints provide access to your organization's global configuration on Field Nation. ## Incomplete Reasons When marking a work order as incomplete, you must provide a reason ID. `GET /company/{company_id}/reasons/incomplete` ### Response ```json { "results": [ { "id": 1, "name": "Provider no-show" }, { "id": 2, "name": "Equipment failure" }, { "id": 3, "name": "Site access denied" }, { "id": 4, "name": "Customer cancellation" } ] } ``` **Usage in API:** ```json DELETE /workorders/{id}/complete?reason=Equipment+failure&reason_id=2 ``` ## Revisit Reasons When a provider must return to a site, specify the reason. `GET /company/{company_id}/reasons/revisit` ```json { "results": [ { "id": 10, "name": "Additional parts needed" }, { "id": 11, "name": "Customer requested callback" }, { "id": 12, "name": "Scope change" } ] } ``` ## Expense Categories Define the types of expenses providers can claim. `GET /company/{company_id}/expenses` ```json { "results": [ { "id": 1, "name": "Materials", "requires_receipt": true }, { "id": 2, "name": "Mileage", "requires_receipt": false }, { "id": 3, "name": "Parking", "requires_receipt": true } ] } ``` ## Tags Tags help organize and filter work orders. `GET /company/{company_id}/tags` ```json { "results": [ { "id": 5, "name": "VIP Customer", "hex_color": "FF0000" }, { "id": 6, "name": "Urgent", "hex_color": "FFA500" }, { "id": 7, "name": "Training", "hex_color": "0000FF" } ] } ``` ## Bonuses & Penalties Global lists of incentives and deductions available for work orders. `GET /bonuses` ```json { "results": [ { "id": 1, "name": "Urgent Dispatch", "amount": 25.00 }, { "id": 2, "name": "Weekend Premium", "amount": 50.00 } ] } ``` `GET /penalties` ```json { "results": [ { "id": 1, "name": "Late Arrival", "amount": -15.00 }, { "id": 2, "name": "Missing Photo", "amount": -10.00 } ] } ``` > [INFO] Most company configuration is managed through the Field Nation UI. Use these API endpoints to retrieve configured values for use in your integration logic. ## Related Resources - [Financials](/docs/rest-api/work-orders/financials/pay) - Using bonuses/penalties on work orders - [Work Orders](/docs/rest-api/work-orders/workflow/lifecycle) - Using incomplete/revisit reasons --- ### Locations URL: /docs/rest-api/resources/locations # Locations Instead of sending full address details with every work order creation request, you can save **Locations** in your address book and reference them by ID. ## Creating a Location `POST /locations` ```json { "name": "Headquarters", "notes": "Main entrance is around the back.", "address1": "730 2nd Ave S", "city": "Minneapolis", "state": "MN", "zip": "55402", "country": "US" } ``` ## Listing Locations `GET /locations` You can search for locations by name or address properties. ## Using a Saved Location When creating a work order, simply provide the `location_id`. ```json { "title": "Fix Server at HQ", "location": { "mode": "saved", "id": 12345 } } ``` > **Consistency**: Using saved locations ensures address data is consistent across all work orders for a specific site, which helps with reporting and history tracking. --- ### Projects URL: /docs/rest-api/resources/projects # Projects **Projects** represent specific initiatives (e.g., "Q4 POS Rollout", "Network Refresh 2024"). They can enforce default templates, pay rates, and selection rules for all work orders assigned to them. ## List Projects `GET /projects` ### Response ```json { "results": [ { "id": 501, "name": "Q4 POS Rollout", "client_id": 102, "active": true, "work_orders_count": 45, "template_id": 88 } ] } ``` ## Create Project `POST /projects` ```json { "name": "Network Refresh 2024", "client_id": 102, "template_id": 88, "description": "Nationwide router replacement initiative" } ``` ## Project Configuration Projects can enforce: | Setting | Description | |---------|-------------| | `template_id` | Default work order template | | `pay_rate` | Standard pay for the project | | `talent_pool_group_id` | Preferred provider group | | `selection_rules` | Auto-assignment criteria | ## Using Projects in Work Orders Assign a work order to a project to inherit its settings: ```json { "title": "Site #44 - Router Install", "project": { "id": 501 }, "client": { "id": 102 } } ``` > [INFO] **Best Practice**: Create projects in the Field Nation UI where you can configure complex settings, then reference the `project_id` in your API integration. ## Adding Providers to Project You can add multiple providers to a project's talent pool: `POST /projects/{project_id}/providers` ```json { "provider_ids": [123, 456, 789] } ``` ## Related Resources - [Clients](/docs/rest-api/resources/clients) - Customer organization - [Talent Pool Groups](/docs/rest-api/resources/talent-pool-groups) - Provider groups for projects - [Templates](/docs/rest-api/resources/templates) - Work order templates --- ### Providers URL: /docs/rest-api/resources/providers # Providers The power of Field Nation is the network of skilled technicians (providers). ## Provider Organization ```mermaid graph TD A[Your Company] --> B[Talent Pool Groups] B --> C[Talent Pool: Tier 1] B --> D[Talent Pool: Tier 2] C --> E[Provider A] C --> F[Provider B] D --> G[Provider C] D --> H[Provider D] A --> I[Marketplace] I --> J[SmartMatch Providers] ``` ## Finding Providers You generally do not "search" for providers by name in the API to route to them blindly. Instead, you utilize **Talent Pools** and **Talent Pool Groups**. ### Marketplace Access Providers are automatically matched to your work orders based on: - Location proximity - Skills and qualifications - Ratings and past performance - Availability ### Direct Provider Lookup If you know a provider's ID (from a previous work order), you can look them up: `GET /users/{user_id}` ```json { "id": 25, "first_name": "Sarah", "last_name": "Johnson", "rating": 4.9, "jobs_completed": 342, "certifications": ["CompTIA A+", "BICSI"] } ``` ## Routing to Providers ### Mass Route to Talent Pool Route a work order to all providers in a talent pool: `POST /workorders/{work_order_id}/mass-route` ```json { "talent_pool_id": 9921 } ``` ### Direct Assignment Assign a specific provider to a work order: `POST /workorders/{work_order_id}/assignee` ```json { "user": { "id": 12345 } } ``` ## Provider Selection When providers are routed a work order, they can: 1. **Request** - Express interest with optional counter-offer 2. **Accept** - Accept as-is (if auto-assign is enabled) 3. **Decline** - Pass on the opportunity > **Best Practice**: Build curated Talent Pools in the Field Nation UI, then reference them by ID in your API integration for consistent routing. ## Related Resources - [Users](/docs/rest-api/resources/users) - User details and profiles - [Talent Pools](/docs/rest-api/resources/talent-pools) - Individual pools of providers - [Talent Pool Groups](/docs/rest-api/resources/talent-pool-groups) - Collections of talent pools --- ### Service territories URL: /docs/rest-api/resources/service-territories # Service Territories Service Territories allow you to define geographic areas and assign specific providers to cover them. This is useful for building regional networks. ## List Service Territories `GET /service-territories` ```json { "results": [ { "id": "st-uuid-1234", "name": "Midwest Region", "description": "MN, WI, IA coverage", "provider_count": 45, "created": { "utc": "2026-01-01 10:00:00" } } ] } ``` ## Create Territory `POST /service-territories` ```json { "name": "Midwest Region", "description": "MN, WI, IA coverage" } ``` ## Get Territory Details `GET /service-territories/{service_territory_uuid}` ```json { "id": "st-uuid-1234", "name": "Midwest Region", "description": "MN, WI, IA coverage", "providers": [ ... ] } ``` ## Update Territory `PUT /service-territories/{service_territory_uuid}` ## Delete Territory `DELETE /service-territories/{service_territory_uuid}` --- ## Managing Providers in Territories ### List Providers `GET /service-territories/{service_territory_uuid}/providers` ### Add Provider Providers are typically added via **Projects** linking to the territory. ### Remove Provider `DELETE /service-territories/{service_territory_uuid}/providers/{provider_id}` **Query Parameters:** - `tierId` (optional): Remove provider from specific talent pool within group. - `removeFromProject` (boolean): If `true`, removes the provider from all service territories in the project. > Either `tierId` or `removeFromProject` is required. ## Usage in Projects Service territories are most powerful when linked to **Projects**. This allows you to auto-route work in a specific zip code to the providers assigned to that territory. ```mermaid graph LR A[Project] --> B[Service Territory] B --> C[Zip Code Match] C --> D[Assigned Providers] D --> E[Auto-Route] ``` ## Related - [Projects](/docs/rest-api/resources/projects) - Linking territories - [Providers](/docs/rest-api/resources/providers) - Managing individual providers - [Talent Pools](/docs/rest-api/resources/talent-pools) - Alternative grouping method --- ### Talent pool groups URL: /docs/rest-api/resources/talent-pool-groups # Talent Pool Groups A **Talent Pool Group** is a collection of talent pools, typically associated with a project. This allows you to route work orders to a broader set of providers with a single ID. ## List Talent Pool Groups `GET /talent-pool-groups` **Query Parameters:** - `f_ids` - Filter by group IDs - `f_name` - Filter by name - `f_project_id` - Filter by project - `limit` - Results per page - `sort_by` - Sort field - `sort_direction` - `asc` or `desc` ### Response ```json { "data": [ { "uuid": "abc-123-def", "name": "Q4 Rollout Team", "project_id": 501, "talent_pools": [ { "id": 1, "name": "Tier 1 - Premium" }, { "id": 2, "name": "Tier 2 - Standard" } ] } ], "pagination": { "has_next_page": false } } ``` ## Create Talent Pool Group `POST /talent-pool-groups` ```json { "name": "New Rollout Team", "project_id": 502, "talent_pools": [ { "name": "Priority Techs" }, { "name": "Backup Techs" } ] } ``` ## Get Talent Pool Group Details `GET /talent-pool-groups/{group_uuid}` Returns detailed information about a specific group including all talent pools and settings. ## Update Talent Pool Group `PUT /talent-pool-groups/{group_uuid}` ```json { "name": "Updated Team Name", "talent_pools": [ { "id": 1, "name": "Renamed Tier 1" }, { "name": "New Tier 3" } ] } ``` ## Delete Talent Pool Group `DELETE /talent-pool-groups/{group_uuid}` > Deleting a talent pool group removes all associated talent pools and provider memberships. This action cannot be undone. --- ## Managing Providers in Groups ### List Providers in a Group `GET /talent-pool-groups/{group_uuid}/providers` **Response:** ```json { "data": [ { "provider_id": 25, "name": "John Smith", "tier": { "id": 1, "name": "Tier 1" }, "exists_in_multiple_talent_pool_groups": false } ] } ``` ### Remove Provider from Group `DELETE /talent-pool-groups/{group_uuid}/providers/{provider_id}` **Query Parameters:** - `tierId` - Remove from specific tier/pool - `removeFromProject` - Remove from all groups in the project ```bash DELETE /talent-pool-groups/{uuid}/providers/25?tierId=1 ``` ```bash DELETE /talent-pool-groups/{uuid}/providers/25?removeFromProject=true ``` ## Related Resources - [Talent Pools](/docs/rest-api/resources/talent-pools) - Individual pools and provider attributes - [Providers](/docs/rest-api/resources/providers) - Provider profiles - [Projects](/docs/rest-api/resources/projects) - Project configuration --- ### Talent pools URL: /docs/rest-api/resources/talent-pools # Talent Pools A **Talent Pool** is a curated list of providers (e.g., "Tier 1 Cabling Techs"). Talent pools are organized within **Talent Pool Groups**. ## Hierarchy ``` Talent Pool Group (Container) ├── Talent Pool (Tier 1) │ ├── Provider A │ └── Provider B ├── Talent Pool (Tier 2) │ └── Provider C └── Talent Pool (General) └── Provider D ``` ## Provider Attributes Each provider in a talent pool can have custom attributes (e.g., pay rate, priority level). ### List Provider Attributes `GET /talent-pools/{talent_pool_id}/providers/{provider_id}/attributes` **Parameters:** - `limit` - Number of results per page - `sort_by` - Field to sort by - `sort_direction` - `asc` or `desc` ### Create Provider Attributes `POST /talent-pools/{talent_pool_id}/providers/{provider_id}/attributes` ```json { "attributes": [ { "name": "pay_rate", "value": "65.00" }, { "name": "priority", "value": "high" } ] } ``` ### Update Provider Attributes `PUT /talent-pools/{talent_pool_id}/providers/{provider_id}/attributes/{attribute_uuid}` ```json { "value": "75.00" } ``` `PUT /talent-pools/{talent_pool_id}/providers/{provider_id}/attributes` ```json { "attributes": [ { "id": "uuid-1", "value": "75.00" }, { "id": "uuid-2", "value": "critical" } ] } ``` ### Delete Provider Attribute `DELETE /talent-pools/{talent_pool_id}/providers/{provider_id}/attributes/{attribute_uuid}` > [INFO] Talent pools are typically created and managed in the Field Nation UI. The API is primarily used for managing provider attributes and automating pool membership. ## Related Resources - [Talent Pool Groups](/docs/rest-api/resources/talent-pool-groups) - Managing groups of talent pools - [Providers](/docs/rest-api/resources/providers) - Provider profiles and routing --- ### Templates URL: /docs/rest-api/resources/templates # Templates **Templates** define reusable work order configurations including tasks, pay structure, and service windows. They ensure consistency across similar jobs. ## List Templates `GET /templates` Retrieve all templates available to your company. ### Response ```json { "results": [ { "id": 88, "name": "Standard Router Install", "description": "2-hour window, fixed pay", "type_of_work": { "id": 15, "name": "Networking" }, "pay": { "type": "fixed", "amount": 125.00 }, "tasks": [ { "name": "Site survey", "required": true }, { "name": "Equipment install", "required": true }, { "name": "Speed test", "required": true } ] } ] } ``` ## Using Templates in Work Orders When creating a work order, reference a template: ```json { "title": "Router Install - Site #44", "template": { "id": 88 }, "location": { "id": 12345 } } ``` The work order inherits: - Pay structure - Tasks and requirements - Service window defaults - Type of work classification ## Template Benefits | Benefit | Description | |---------|-------------| | **Consistency** | All WOs for same job type have identical requirements | | **Efficiency** | Reduced API payload - just send template ID | | **Governance** | Centralized control over job specifications | > **Best Practice**: Create and maintain templates in the Field Nation UI. Use the API to list available templates and reference them by ID when creating work orders. ## Template vs. Direct Creation ApproachWhen to Use Template ID Standardized, repeatable jobs (recommended) Full Payload Unique, one-off jobs with custom requirements ## Related Resources - [Types of Work](/docs/rest-api/resources/types-of-work) - Job classifications - [Work Order Creation](/docs/rest-api/work-orders/basics/create) - Using templates --- ### Types of work URL: /docs/rest-api/resources/types-of-work # Types of Work **Types of Work** classify the nature of a job (e.g., "Networking", "Point of Sale", "Cabling"). This classification helps: - Match work orders with qualified providers - Organize reporting by job category - Apply appropriate pay rates ## List Types of Work `GET /types-of-work` Retrieve all types of work available in the marketplace. ### Response ```json { "results": [ { "id": 15, "name": "Networking", "description": "Router, switch, and firewall installation/configuration" }, { "id": 22, "name": "Point of Sale", "description": "POS terminal installation and setup" }, { "id": 31, "name": "Structured Cabling", "description": "Cat5e/Cat6 cable runs and terminations" } ] } ``` ## Using Types of Work When creating a work order, specify the type: ```json { "title": "Install Firewall", "type_of_work": { "id": 15 } } ``` ## Provider Matching Types of Work influence SmartMatch: | Factor | How It's Used | |--------|---------------| | **Skills** | Providers self-certify expertise | | **History** | Past performance on similar jobs | | **Certifications** | Industry credentials (e.g., BICSI) | > [INFO] Types of Work are maintained by Field Nation. You cannot create custom types, but you can select from the comprehensive list available via the API. ## Common Types | ID | Name | Common Use Cases | |----|------|-----------------| | 1 | General IT | Broad IT support tasks | | 15 | Networking | Routers, switches, firewalls | | 22 | Point of Sale | POS terminals, payment systems | | 31 | Structured Cabling | Cable installation, termination | | 45 | Desktop Support | PC setup, troubleshooting | ## Related Resources - [Templates](/docs/rest-api/resources/templates) - Templates include type of work - [Work Order Creation](/docs/rest-api/work-orders/basics/create) - Using types when creating WOs --- ### Users URL: /docs/rest-api/resources/users # Users Every account on Field Nation is a **User**. This includes internal staff (Admins, Dispatchers) and external **Providers** (technicians). ## Get User Details Retrieve detailed information about a specific user by their ID. `GET /users/{user_id}` ### Response ```json { "id": 12345, "first_name": "John", "last_name": "Smith", "email": "john.smith@example.com", "phone": "+1-555-123-4567", "thumbnail": "https://...", "rating": 4.8, "jobs_completed": 156, "user_type": "provider" } ``` > [INFO] **Providers are Users**: All providers in the marketplace are users. Use the user_id to look up provider details, assign work orders, or add them to talent pools. ## User Types | Type | Description | |------|-------------| | `provider` | External technician in the marketplace | | `buyer` | Staff member on a buyer company | | `admin` | Administrator with full access | ## Related Endpoints - [Providers](/docs/rest-api/resources/providers) - Finding and managing providers - [Talent Pools](/docs/rest-api/resources/talent-pools) - Organizing providers into groups --- ### Overview URL: /docs/rest-api/work-orders/overview # Work Orders Overview A **Work Order** is the fundamental unit of work in the Field Nation ecosystem. It represents a job to be done, a contract between a Buyer and a Provider, and a record of the work performed. This section covers the **Client API** endpoints for creating and managing work orders. ## Core Concepts Before diving into the endpoints, it's crucial to understand a few key terms: | Term | Definition | |------|------------| | **Buyer** | You (or your client). The entity creating the work order and paying for the work. | | **Provider** | The technician or contractor who performs the work. | | **Marketplace** | The open network of providers. You can "Route" work to the marketplace to find new talent. | | **Private Network** | Your curated list of preferred providers. | | **Draft** | The initial state of a work order. Visible only to you. | | **Routed** | The state where work is available for providers to request. | | **Assigned** | The state where a specific provider is contracted to do the work. | ## The Lifecycle A work order acts as a state machine. You move it through specific stages using API actions. ```mermaid stateDiagram-v2 direction LR Draft --> Published: Publish Published --> Routed: Route Routed --> Assigned: Assign Assigned --> WorkDone: Complete WorkDone --> Approved: Approve Approved --> [*]: Pay ``` 1. **Create**: Build the definition of work (Scope, Location, Pay). 2. **Publish**: Make it "live" (but not yet visible to specific people). 3. **Route**: Send it to a specific provider, a group, or the marketplace. 4. **Assign**: Confirm the contract with a specific provider. 5. **Manage**: The provider does the work (Check-in, Uploads, Check-out). 6. **Approve**: You verify the work and release payment. ## Before You Begin To successfully create and manage work orders, you will need to reference other resources. **You cannot usually just "guess" IDs.** ### 1. Get Your Tokens Ensure you have a valid Oauth2 access token. ### 2. Fetch Metadata Most dropdowns or ID fields in the Work Order payload come from these lookup endpoints: - **Types of Work**: `GET /types-of-work` (Required for creation) - **Service Contracts**: `GET /service-contracts` (Who pays for this?) - **Projects**: `GET /projects` (Optional organization) ### 3. Build Your Network If you plan to route to specific people, you need their IDs: - **Providers**: `GET /providers` or `GET /talent-pools` ## Next Steps Start by learning how to construct the work order payload. [Create Work Order](/docs/rest-api/work-orders/basics/create) --- ### Create URL: /docs/rest-api/work-orders/basics/create # Creating Work Orders The core action of the Client API is creating work orders. This is done via a `POST` request to `/workorders`. ## Endpoint ## Prerequisites Before creating a work order, ensure you have the necessary IDs. You cannot "makes up" these values; they must exist in the system. > [INFO] **Where to find IDs:** - `types_of_work`: Call `GET /types-of-work` - `template`: Call `GET /templates` (or create one in the UI) - `project`: Call `GET /projects` - `location`: Call `GET /locations` (for saved locations) ## Payload Structure A work order is composed of several key objects. You do not need to provide all of them if you use a [Template](/docs/rest-api/resources/templates). ### 1. Basic Info Title, Description, and Classification. ```json { "title": "Replace POS Terminal", "description": { "html": "

Swap out the defective unit.

" }, "types_of_work": [{ "id": 62, "isPrimary": true }] } ``` ### 2. Location Where the work acts. ```json "location": { "mode": "custom", "address1": "123 Main St", "city": "Minneapolis", "state": "MN", "zip": "55401", "country": "US" } ``` ### 3. Schedule When the work should be done. Three schedule modes are available: - **exact**: Must arrive at a specific time - **between**: Arrival within a time window - **hours**: Complete within specified hours from start ```json "schedule": { "service_window": { "mode": "exact", "start": { "utc": "2026-01-15 09:00:00" } } } ``` For Hard Start (strict on-time requirement), add `require_ontime`: ```json { "schedule": { "service_window": { "mode": "exact", "start": { "utc": "2026-01-15 09:00:00" } } }, "require_ontime": true } ``` ### 4. Pay How much you are offering. ```json "pay": { "type": "fixed", "base": { "amount": 150.00, "units": 1 } } ``` ## Complete Examples **Scenario**: Simple job at a specific time with a flat rate. ```json { "title":"Install Point of Sale", "types_of_work":[{ "id": 62, "isPrimary": true }], "location":{ "mode":"custom", "address1":"123 Main Street", "city":"Phoenix", "state":"AZ", "zip":"85001", "country":"US" }, "schedule":{ "service_window":{ "mode":"exact", "start":{ "utc":"2026-01-15 13:00:00" } } }, "pay":{ "type":"fixed", "base":{ "amount":400, "units":1 } } } ``` **Scenario**: Rate per hour, with a "Between" service window (Open window). ```json { "title":"Troubleshoot Network", "types_of_work":[{ "id": 76, "isPrimary": true }], "location":{ "mode":"custom", "address1":"123 Main Street", "city":"Phoenix", "state":"AZ", "zip":"85001" }, "schedule":{ "service_window":{ "mode":"between", "start":{ "utc":"2026-01-15 09:00:00" }, "end":{ "utc":"2026-01-15 17:00:00" } } }, "pay":{ "type":"hourly", "base":{ "amount":45, "units":2 } } } ``` **Scenario**: Using a Template ID (`68`) to pre-fill description and settings. ```json { "title":"Standard Maintenance", "template":{ "id":68 }, "location":{ "mode":"custom", "address1":"123 Main Street", "city":"Phoenix", "state":"AZ", "zip":"85001" }, "schedule":{ "service_window":{ "mode":"exact", "start":{ "utc":"2026-01-15 13:00:00" } } } } ``` ## Advanced Options You can pass custom field values if your project requires them. ```json "custom_fields": { "results": [ { "results": [ { "id": 129, "value": "Store #5521" } ] } ] } ``` Add incentives or requirements to the pay structure. ```json "pay": { "type": "fixed", "base": { "amount": 100, "units": 1 }, "bonuses": { "results": [{ "id": 3 }] } } ``` > **Validation**: Ensure your `types_of_work` IDs and `template` IDs are valid in the Production environment, as they differ from Sandbox. ## Field Reference ', required: true, }, location: { description: 'Job location (custom or saved)', type: 'LocationObject', required: true, }, pay: { description: 'Payment terms (fixed or hourly)', type: 'PayObject', required: true, }, schedule: { description: 'Service window timing', type: 'ScheduleObject', required: true, }, description: { description: 'HTML-formatted job description', type: '{ html: string }', required: false, }, template: { description: 'Use template to pre-fill fields', type: '{ id: number }', required: false, }, project: { description: 'Assign to project', type: '{ id: number }', required: false, }, client: { description: 'Tag with client', type: '{ id: number }', required: false, }, }} /> ## Related - [Templates](/docs/rest-api/resources/templates) - Pre-fill work order fields - [Types of Work](/docs/rest-api/resources/types-of-work) - Job classifications - [Locations](/docs/rest-api/resources/locations) - Saved locations - [Workflow](/docs/rest-api/work-orders/workflow/lifecycle) - Next: publish and route --- ### Operations URL: /docs/rest-api/work-orders/basics/operations # Update & Cancel Beyond the happy-path workflow, you will need to manage details, updates, and cancellations. ## Updating Work Orders You can update almost any field of a work order (Schedule, Scope, Pay) as long as it isn't in a "locked" state (like `Paid`). `PUT /workorders/{id}` Only send the fields you want to change (Partial Update). ```json { "schedule": { "service_window": { "start": { "utc": "2023-12-01 10:00:00" } } } } ``` ### Updateable Fields | Field | Description | |-------|-------------| | `title` | Work order title | | `description` | Job description (`{ html: string }`) | | `schedule` | Service window and timing | | `require_ontime` | Require on-time arrival (Hard Start) | | `pay` | Pay amount and type | | `location` | Job location | | `contacts` | Site contacts | | `tasks` | Checklist tasks | | `custom_fields` | Custom field values | | `project` | Project assignment (`{ id: number }`) | | `client` | Client tag (`{ id: number }`) | | `manager` | Manager assignment (`{ id: number }`) | | `allow_counter_offers` | Allow provider counter offers | | `require_gps` | Require GPS check-in | ## Canceling Work Orders To cancel a work order, use the DELETE method. You must provide a reason. `DELETE /workorders/{id}?cancel_reason=Duplicate` > **Fees**: Canceling a work order *after* a provider has been assigned and dispatched may incur a cancellation fee depending on platform rules. ## Get Work Order Details Retrieve full details of a specific work order. `GET /workorders/{id}` ### Response Includes - Basic info (title, description, status) - Location details - Schedule and service window - Pay structure - Assigned provider - Tasks, signatures, time logs - Messages and attachments --- ### Search URL: /docs/rest-api/work-orders/basics/search # Search & Filter Work Orders `GET /workorders` is your main tool for retrieving work orders. Think of it like the Field Nation dashboard's search and filter panel — but as an API. You describe what you want using query parameters, and the API returns exactly that. ```http GET /workorders ``` --- ## How It Works ### Pick a List A **list** scopes your results to a stage of the work order lifecycle — the same as clicking a tab in the Field Nation UI (e.g., "Assigned" or "Draft"). Each list pre-applies its own base filters, so you always start from the right context. ```bash curl "https://api.fieldnation.com/api/rest/v2/workorders?list=workorders_assigned&access_token=YOUR_TOKEN" ``` ### Add Filters Layer any number of `f_*` parameters to narrow results. Filters combine with AND logic — every filter you add further reduces the result set. ```bash curl "https://api.fieldnation.com/api/rest/v2/workorders?list=workorders_assigned&f_state=TX&f_service_schedule=2026-01-01,2026-01-31&access_token=YOUR_TOKEN" ``` ### Paginate Through Results Use `page` and `per_page` to work through large result sets. The response always tells you the total count and how many pages exist. ```bash curl "https://api.fieldnation.com/api/rest/v2/workorders?list=workorders_all&f_state=TX&page=2&per_page=50&access_token=YOUR_TOKEN" ``` --- ## Lists A list scopes your results to a specific stage of the work order lifecycle. Every request should include a `list` parameter — without it, the API may fall back to a previously saved state and return unexpected results. | Value | Label | What's in it | | :--- | :--- | :--- | | `workorders_in_flight` | In-Flight | Active work orders that are underway or confirmed | | `workorders_draft` | Draft | Unpublished work orders still being configured | | `workorders_published_routed` | Published / Routed | Published or routed to providers, not yet assigned | | `workorders_assigned` | Assigned | Work orders with a confirmed provider assignment | | `workorders_problem` | Issue | Work orders with a reported problem | | `workorders_work_done` | Done | Provider has marked the work complete | | `workorders_approved` | Approved | You have approved the completed work | | `workorders_all` | All | Every non-archived work order *(API default)* | ```bash curl "https://api.fieldnation.com/api/rest/v2/workorders?list=workorders_assigned&sticky=false&access_token=YOUR_TOKEN" ``` > [INFO] **Integration tip**: Always pass `list` explicitly in your integration. If omitted, the API uses the last saved list from your account's sticky state, which can silently change the result set between requests. --- ## Keyword Search Not sure of the exact work order details? Use `f_search` to run a free-form keyword search across multiple fields at once. ```bash curl "https://api.fieldnation.com/api/rest/v2/workorders?list=workorders_all&f_search=network+switch+installation&sticky=false&access_token=YOUR_TOKEN" ``` --- ## Filters All filter parameters are prefixed with `f_` and can be combined freely. The sections below cover the most commonly used filters, grouped by what they filter. ### Work Order Use these to find specific work orders by ID, template, type, or label. ```bash curl "https://api.fieldnation.com/api/rest/v2/workorders?list=workorders_all&f_work_order_id=1001,1002,1003&sticky=false&access_token=YOUR_TOKEN" ``` ```bash curl "https://api.fieldnation.com/api/rest/v2/workorders?list=workorders_all&f_template=55&sticky=false&access_token=YOUR_TOKEN" ``` ```bash curl "https://api.fieldnation.com/api/rest/v2/workorders?list=workorders_all&f_type_of_work=Networking,Cabling&sticky=false&access_token=YOUR_TOKEN" ``` ```bash # Returns only work orders with no flags assigned curl "https://api.fieldnation.com/api/rest/v2/workorders?list=workorders_all&f_flags=&sticky=false&access_token=YOUR_TOKEN" ``` --- ### Assignment & Provider Use these to filter by who is assigned, how they were dispatched, or whether there are outstanding requests or counter-offers. ```bash curl "https://api.fieldnation.com/api/rest/v2/workorders?list=workorders_assigned&f_assigned_provider=12345&sticky=false&access_token=YOUR_TOKEN" ``` ```bash curl "https://api.fieldnation.com/api/rest/v2/workorders?list=workorders_published_routed&f_requests=true&sticky=false&access_token=YOUR_TOKEN" ``` ```bash curl "https://api.fieldnation.com/api/rest/v2/workorders?list=workorders_all&f_has_counter_offer=true&sticky=false&access_token=YOUR_TOKEN" ``` ```bash # Work orders assigned to providers rated 4.0 stars or higher curl "https://api.fieldnation.com/api/rest/v2/workorders?f_rating=4.0&sticky=false&access_token=YOUR_TOKEN" ``` --- ### Organization Filter by internal structure — the company, manager, client, project, network, or funding account associated with a work order. ```bash # Work orders for a specific client under a project curl "https://api.fieldnation.com/api/rest/v2/workorders?list=workorders_all&f_client=20&f_project=55&sticky=false&access_token=YOUR_TOKEN" # Work orders managed by a specific person curl "https://api.fieldnation.com/api/rest/v2/workorders?list=workorders_all&f_manager=jane@acme.com&sticky=false&access_token=YOUR_TOKEN" # Work orders funded by a specific account curl "https://api.fieldnation.com/api/rest/v2/workorders?list=workorders_all&f_fund=10&sticky=false&access_token=YOUR_TOKEN" ``` --- ### Dates Filter work orders by when key events occurred. All date filters follow the same format. > [INFO] **Date format**: Dates are accepted as `YYYY-MM-DD` for a full calendar day, or `YYYY-MM-DD,YYYY-MM-DD` for an inclusive range. All values are treated as UTC and converted to each work order's local time zone for comparison. ```bash # Work orders scheduled in January 2026 curl "https://api.fieldnation.com/api/rest/v2/workorders?f_service_schedule=2026-01-01,2026-01-31&sticky=false&access_token=YOUR_TOKEN" ``` ```bash # Work orders created on a specific day curl "https://api.fieldnation.com/api/rest/v2/workorders?f_created_date=2026-03-01&sticky=false&access_token=YOUR_TOKEN" # Work orders created in Q1 2026 curl "https://api.fieldnation.com/api/rest/v2/workorders?f_created_date=2026-01-01,2026-03-31&sticky=false&access_token=YOUR_TOKEN" ``` ```bash # Work orders approved or cancelled in Q1 2026 curl "https://api.fieldnation.com/api/rest/v2/workorders?f_approved_cancelled_date=2026-01-01,2026-03-31&sticky=false&access_token=YOUR_TOKEN" ``` ```bash # Work orders marked complete in February 2026 curl "https://api.fieldnation.com/api/rest/v2/workorders?f_work_done_done=2026-02-01,2026-02-28&sticky=false&access_token=YOUR_TOKEN" ``` --- ### Location Filter by the physical location of the work order. Use geographic fields for broad filtering, or saved location IDs for precise, pre-defined sites. ```bash curl "https://api.fieldnation.com/api/rest/v2/workorders?f_state=TX,CA&sticky=false&access_token=YOUR_TOKEN" ``` ```bash curl "https://api.fieldnation.com/api/rest/v2/workorders?f_city=Austin,Dallas&sticky=false&access_token=YOUR_TOKEN" ``` ```bash curl "https://api.fieldnation.com/api/rest/v2/workorders?f_zip=78701,78702&sticky=false&access_token=YOUR_TOKEN" ``` ```bash # One or more specific saved locations curl "https://api.fieldnation.com/api/rest/v2/workorders?f_location_ids=42,43&sticky=false&access_token=YOUR_TOKEN" # A saved location group curl "https://api.fieldnation.com/api/rest/v2/workorders?f_location_group_ids=7&sticky=false&access_token=YOUR_TOKEN" ``` ```bash # By IANA time zone name curl "https://api.fieldnation.com/api/rest/v2/workorders?f_time_zone=America/Chicago&sticky=false&access_token=YOUR_TOKEN" # By UTC offset range (covers Central and Eastern) curl "https://api.fieldnation.com/api/rest/v2/workorders?f_time_zone=-5,-6&sticky=false&access_token=YOUR_TOKEN" ``` --- ### Pay Filter by how much a work order pays. Accepts a single minimum value or an inclusive range. ```bash # Work orders paying between $100 and $500 total curl "https://api.fieldnation.com/api/rest/v2/workorders?list=workorders_all&f_pay=100,500&sticky=false&access_token=YOUR_TOKEN" # Work orders paying at least $250 total curl "https://api.fieldnation.com/api/rest/v2/workorders?list=workorders_all&f_pay=250&sticky=false&access_token=YOUR_TOKEN" # Work orders with an hourly rate of at least $50/hr curl "https://api.fieldnation.com/api/rest/v2/workorders?list=workorders_all&f_min_hourly_rate=50&sticky=false&access_token=YOUR_TOKEN" ``` --- ## Pagination ```bash # Fetch the second page with 50 results per page curl "https://api.fieldnation.com/api/rest/v2/workorders?list=workorders_all&page=2&per_page=50&sticky=false&access_token=YOUR_TOKEN" ``` The response `metadata` object always includes `total`, `page`, `pages`, and `per_page` so you know how many more pages remain. --- ## Sorting ```bash # Soonest service date first curl "https://api.fieldnation.com/api/rest/v2/workorders?list=workorders_all&sort=schedule&order=asc&sticky=false&access_token=YOUR_TOKEN" # Most recently created first curl "https://api.fieldnation.com/api/rest/v2/workorders?list=workorders_all&sort=created_date&order=desc&sticky=false&access_token=YOUR_TOKEN" # Highest paying first curl "https://api.fieldnation.com/api/rest/v2/workorders?list=workorders_all&sort=pay&order=desc&sticky=false&access_token=YOUR_TOKEN" ``` --- ## Advanced Options Control the shape and columns returned in the response. Most integrations can leave these at their defaults. ```bash curl "https://api.fieldnation.com/api/rest/v2/workorders?view=list&columns=id,title,status,schedule&sticky=false&access_token=YOUR_TOKEN" ``` Sticky parameters persist your filter, column, and sort preferences server-side against your user account and the active `list`. Pagination is **never** persisted. > **Always pass sticky=false in integrations**: When `sticky` is omitted or `true`, filters are saved server-side against your user account. A filter applied in one request can silently carry over into a subsequent request that does not include it — causing unexpected, hard-to-debug results. Always pass `sticky=false` to keep each request fully self-contained. You can verify which filters were actually applied by inspecting the response body. Every recognized filter is echoed back as an `f_*` key with a corresponding `fs_*` boolean: ```json { "f_service_schedule": "2026-01-01,2026-01-31", "fs_service_schedule": true } ``` If a filter key is absent from the response, it was not recognized by the API. Pass `f_=` (the bare `f_` parameter with an empty value) to clear all saved sticky filters for the current list. ```bash curl "https://api.fieldnation.com/api/rest/v2/workorders?list=workorders_all&f_=&sticky=false&access_token=YOUR_TOKEN" ``` This is useful when resetting state after filters were accidentally persisted by a previous request. --- ## Common Recipes Real-world examples combining multiple parameters. **All open work orders in Texas, sorted by service date:** ```bash curl "https://api.fieldnation.com/api/rest/v2/workorders?list=workorders_all&f_state=TX&sort=schedule&order=asc&sticky=false&access_token=YOUR_TOKEN" ``` **Work orders created in Q1 2026 for a specific project:** ```bash curl "https://api.fieldnation.com/api/rest/v2/workorders?list=workorders_all&f_project=55&f_created_date=2026-01-01,2026-03-31&sticky=false&access_token=YOUR_TOKEN" ``` **Published work orders with pending provider requests, page 2:** ```bash curl "https://api.fieldnation.com/api/rest/v2/workorders?list=workorders_published_routed&f_requests=true&page=2&per_page=25&sticky=false&access_token=YOUR_TOKEN" ``` **All work orders assigned to a specific provider:** ```bash curl "https://api.fieldnation.com/api/rest/v2/workorders?list=workorders_assigned&f_assigned_provider=12345&sticky=false&access_token=YOUR_TOKEN" ``` **Keyword search across all draft work orders:** ```bash curl "https://api.fieldnation.com/api/rest/v2/workorders?list=workorders_draft&f_search=fiber+optic&sticky=false&access_token=YOUR_TOKEN" ``` **Work orders for a specific client, filtered by pay range:** ```bash curl "https://api.fieldnation.com/api/rest/v2/workorders?list=workorders_all&f_client=20&f_pay=100,500&sticky=false&access_token=YOUR_TOKEN" ``` --- ## Response Structure A successful `200` response always includes these top-level fields. Array of work order objects matching your query. The shape of each object depends on the `view` and `columns` parameters you passed. Pagination context and active state for the current response. All available list tabs with their labels and counts — mirrors the tab bar in the Field Nation UI. Useful for displaying counts across multiple lifecycle stages at once. The authenticated user's saved custom filter sets. These can be applied by name to quickly restore a complex filter combination. ```json title="Example response shape" { "results": [...], "metadata": { "total": 142, "page": 1, "pages": 6, "per_page": 25, "sort": "schedule", "order": "asc", "list": "workorders_all", "available_filters": [...], "available_columns": [...] }, "lists": [...], "saved_filters": [...] } ``` --- ### Attachments URL: /docs/rest-api/work-orders/communication/attachments # Attachments Upload documents, photos, or diagrams to a work order. Files are organized into folders. ## List Attachments `GET /workorders/{id}/attachments` ### Response ```json { "results": [ { "id": 1, "name": "Work Instructions", "results": [ { "id": 101, "file": { "name": "install_guide.pdf", "link": "https://cdn.fieldnation.com/files/abc.pdf", "size_bytes": 245000 }, "created": { "utc": "2026-01-15 10:00:00" } } ] }, { "id": 2, "name": "Completion Photos", "results": [] } ] } ``` ## Create Folder `POST /workorders/{id}/attachments` ```json { "name": "Site Documentation" } ``` ## Upload File `POST /workorders/{id}/attachments/{folder_id}` **Content-Type**: `multipart/form-data` ```bash curl -X POST \ -F "file=@/path/to/document.pdf" \ "https://api.fieldnation.com/api/rest/v2/workorders/{id}/attachments/{folder_id}?access_token={token}" ``` ```javascript const formData = new FormData(); formData.append('file', fileBlob, 'document.pdf'); fetch(`/workorders/${id}/attachments/${folderId}?access_token=${token}`, { method: 'POST', body: formData }); ``` ## Update File `PUT /workorders/{id}/attachments/{folder_id}/{attachment_id}` ```json { "filename": "updated_guide.pdf", "visible_to_provider": true } ``` ## Delete File `DELETE /workorders/{id}/attachments/{folder_id}/{attachment_id}` ## Supported File Types | Category | Extensions | |----------|------------| | Images | `.jpg`, `.jpeg`, `.png`, `.gif` | | Documents | `.pdf`, `.doc`, `.docx` | | Spreadsheets | `.xls`, `.xlsx`, `.csv` | > Maximum file size is typically 10MB. Large files may need to be compressed. ## Provider-Uploaded Photos Providers typically upload completion photos via the mobile app. These appear in designated folders and are used for work verification. ## Related - [Messages](/docs/rest-api/work-orders/communication/messages) - Text communication - [Tasks](/docs/rest-api/work-orders/execution/tasks) - Photo tasks --- ### Contacts URL: /docs/rest-api/work-orders/communication/contacts # Contacts Contacts provide the provider with on-site contact information - who to call when they arrive, who can grant access, etc. ## List Contacts `GET /workorders/{id}/contacts` ### Response ```json { "results": [ { "id": 101, "name": "John Doe", "role": "Location Contact", "phone": "+1-555-123-4567", "ext": "101", "email": "john.doe@store.com", "notes": "Available Mon-Fri 9am-5pm" }, { "id": 102, "name": "Jane Smith", "role": "Technical help", "phone": "+1-555-987-6543", "email": "jane.smith@store.com" } ] } ``` ## Create Contact `POST /workorders/{id}/contacts` ```json { "name": "John Doe", "role": "Location Contact", "phone": "+1-555-123-4567", "ext": "101", "email": "john.doe@store.com", "notes": "Available during business hours" } ``` ## Update Contact `PUT /workorders/{id}/contacts/{contact_id}` ```json { "phone": "+1-555-999-8888" } ``` ## Delete Contact `DELETE /workorders/{id}/contacts/{contact_id}` > [INFO] At least one contact should remain on the work order so the provider has someone to reach on-site. ## Contact Roles Available contact roles (use exact values): | Role | Description | |------|-------------| | `Project manager` | Primary project authority | | `Location Contact` | On-site contact person | | `Resource coordinator` | Resource scheduling | | `Emergency contact` | Emergency situations | | `Technical help` | Technical questions | | `Check-in / Check-out` | Check-in verification | ## Phone Visibility Contact phone numbers are visible to assigned providers. For privacy: - Use work phones, not personal numbers - Remove contacts after work completion if needed ## Contact at Creation Include contacts when creating a work order: ```json { "title": "Router Installation", "contacts": { "results": [ { "name": "John Doe", "role": "Location Contact", "phone": "+1-555-123-4567", "notes": "Ask for IT department" } ] } } ``` ## Related - [Signatures](/docs/rest-api/work-orders/execution/signatures) - Contacts may need to sign - [Messages](/docs/rest-api/work-orders/communication/messages) - Communicate about contacts --- ### Messages URL: /docs/rest-api/work-orders/communication/messages # Messages Communicate with the provider via work order messages. Messages are visible to specified groups and create a communication history. ## List Messages `GET /workorders/{id}/messages` ### Response ```json { "results": [ { "msg_id": "1001", "from": { "id": 12345, "name": "John Smith", "role": "provider" }, "message": "On my way to the site now.", "role": "ASSIGNEDTECH", "read": true, "created": { "utc": "2026-01-15 08:30:00" } }, { "msg_id": "1002", "from": { "id": 500, "name": "Dispatch Team", "role": "buyer" }, "message": "Please use the side entrance.", "role": "ASSIGNEDTECH", "read": false, "created": { "utc": "2026-01-15 08:00:00" } } ] } ``` ## Send Message `POST /workorders/{id}/messages` ```json { "message": "Please use the side entrance.", "type": "ASSIGNEDTECH" } ``` ## Reply to Message `POST /workorders/{id}/messages/{message_id}` ```json { "message": "Understood, heading there now." } ``` ## Message Types Control message visibility and recipients using the `type` field: | Type | Description | |------|------------| | `INTERNAL` | Internal team only (Staff Note) | | `ASSIGNEDTECH` | Send to assigned provider | | `REQUESTEDTECHS` | Send to all requested providers | | `SPECIFICTECH` | Send to specific provider (use `to.id`) | | `ROUTEDTECHS` | Send to all routed providers | | `REPLY` | Reply to a message | | `REPLYTOTHREAD` | Reply to a message thread | | `TECHTOMANAGER` | Provider to manager communication | | `STAFF_NOTE` | Internal staff note | ```json { "message": "Equipment is in the back room.", "type": "ASSIGNEDTECH" } ``` ```json { "message": "Provider seems inexperienced, monitor closely.", "type": "INTERNAL" } ``` > [INFO] `INTERNAL` and `STAFF_NOTE` messages are only visible to your internal team, not to the provider. ## Message Notifications Messages trigger notifications to relevant parties: - Push notifications to mobile app - Email notifications (based on preferences) - In-app alerts on work order ## Related - [Attachments](/docs/rest-api/work-orders/communication/attachments) - File sharing - [Contacts](/docs/rest-api/work-orders/communication/contacts) - Site contact info --- ### Compliance URL: /docs/rest-api/work-orders/execution/compliance # Compliance & Qualifications Certain work requires specific certifications or qualifications. You can enforce these on a work order to ensure only qualified providers can accept. ## List Qualifications `GET /workorders/{id}/qualifications` ### Response ```json { "results": [ { "id": 45, "name": "CompTIA A+", "required": true, "type": "certification" }, { "id": 67, "name": "Background Check", "required": true, "type": "screening" } ] } ``` ## Add Qualification Requirement `POST /workorders/{id}/qualifications` ```json { "qualification_id": 45, "required": true } ``` ## Remove Qualification `DELETE /workorders/{id}/qualifications/{qualification_id}` ## Qualification Types | Type | Description | Examples | |------|-------------|----------| | `certification` | Industry credentials | CompTIA A+, BICSI, Cisco | | `screening` | Background checks | Drug test, Criminal check | | `insurance` | Coverage requirements | Liability insurance | | `tool` | Equipment ownership | Ladder, specific tools | ## Provider Verification When routing, providers are automatically filtered by qualifications: ```json { "selection_rule": { "rules": [ { "name": "qualification", "value": 45 } ] } } ``` > [INFO] Providers must have uploaded and verified their qualifications on Field Nation before they can request work requiring those qualifications. ## Problems & Issues Track problems reported during work execution. `GET /workorders/{id}/problems` ### Create Problem `POST /workorders/{id}/problems` ```json { "problem_type_id": 5, "description": "Site not ready - equipment not delivered" } ``` ### Update Problem Status `PUT /workorders/{id}/problems/{problem_id}` ```json { "status": "resolved" } ``` ## Related - [Tasks](/docs/rest-api/work-orders/execution/tasks) - Work completion checklist - [Time Logs](/docs/rest-api/work-orders/execution/time-logs) - Hour tracking --- ### Custom fields URL: /docs/rest-api/work-orders/execution/custom-fields # Custom Fields Custom fields allow you to capture additional data on work orders beyond the standard fields. There are two types: - **Buyer Custom Fields**: Values set by the buyer when creating/updating the work order - **Provider Custom Fields**: Values the provider fills out during work completion ## Custom Field Types ```mermaid graph LR A[Custom Fields] --> B[Buyer Fields] A --> C[Provider Fields] B --> D[Set at WO Creation] B --> E[Updated by Buyer] C --> F[Filled by Provider] C --> G[Required for Completion] ``` --- ## Work Order Custom Fields ### List Custom Fields on Work Order `GET /workorders/{work_order_id}/custom_fields` Returns all custom fields and their values for a specific work order. ```json { "results": [ { "id": 129, "name": "Store Number", "type": "text", "value": "5521", "required": true, "filled_by": "buyer" }, { "id": 130, "name": "Serial Number", "type": "text", "value": null, "required": true, "filled_by": "provider" } ] } ``` ### Get Specific Custom Field `GET /workorders/{work_order_id}/custom_fields/{custom_field_id}` ```json { "id": 129, "name": "Store Number", "type": "text", "value": "5521", "required": true, "filled_by": "buyer" } ``` ### Update Custom Field Value `PUT /workorders/{work_order_id}/custom_fields/{custom_field_id}` Update a buyer-defined custom field: ```json { "value": "5522" } ``` Provider updates their required field: ```json { "value": "SN-ABC-123456" } ``` --- ## Global Custom Fields (Company) ### List All Custom Fields Get all custom fields defined for your company. `GET /custom-fields` ```json { "results": [ { "id": 129, "name": "Store Number", "type": "text", "required": true, "filled_by": "buyer", "projects": [501, 502] }, { "id": 130, "name": "Serial Number Captured", "type": "text", "required": true, "filled_by": "provider", "projects": [501] }, { "id": 131, "name": "Installation Type", "type": "dropdown", "options": ["New Install", "Replacement", "Upgrade"], "required": false, "filled_by": "buyer", "projects": [] } ] } ``` ## Custom Field Types ## Setting Custom Fields at Creation When creating a work order, include custom field values: ```json { "title": "Install POS Terminal", "custom_fields": { "results": [ { "results": [ { "id": 129, "value": "Store #5521" } ] } ] } } ``` > [INFO] Custom fields are typically configured in the Field Nation UI and associated with specific projects. Use the API to read the configuration and set/update values. ## Provider Custom Fields Provider custom fields are filled out by the technician during or after completing work. These are often used for: - Capturing serial numbers - Recording measurements - Confirming checklist items - Documenting site conditions > If a provider custom field is marked as `required`, the work order cannot be marked complete until the provider fills in the value. ## Related - [Tasks](/docs/rest-api/work-orders/execution/tasks) - Task completion requirements - [Create Work Order](/docs/rest-api/work-orders/basics/create) - Using custom fields at creation - [Projects](/docs/rest-api/resources/projects) - Project-level custom fields --- ### Milestones URL: /docs/rest-api/work-orders/execution/milestones # Milestones Milestones provide a detailed audit trail of key events in the work order's life. ## List Milestones `GET /workorders/{id}/milestones` ```json { "results": [ { "name": "published", "label": "Published", "completed": true, "completed_at": { "utc": "2026-01-10 09:00:00" }, "actor": { "name": "System" } }, { "name": "assigned", "label": "Assigned", "completed": true, "completed_at": { "utc": "2026-01-11 14:00:00" }, "actor": { "name": "Dispatch Team" } }, { "name": "checked_in", "label": "Checked In", "completed": false, "completed_at": null } ] } ``` ## Milestone Types Common milestones tracked: - `created` - `published` - `routed` - `assigned` - `confirmed` (Provider confirmed schedule) - `checked_in` - `checked_out` - `work_done` - `approved` - `paid` > [INFO] Milestones are read-only and automatically updated by system actions. They are excellent for building timeline visualizations in your integration. ## Related - [Lifecycle](/docs/rest-api/work-orders/workflow/lifecycle) - The states that drive milestones - [Time Logs](/docs/rest-api/work-orders/execution/time-logs) - Detailed time tracking --- ### Signatures URL: /docs/rest-api/work-orders/execution/signatures # Signatures Providers often need to capture signatures (from site contacts) to prove work completion. Digital signatures are stored and accessible via the API. ## List Signatures `GET /workorders/{id}/signatures` ### Response ```json { "results": [ { "id": 101, "name": "John Doe", "data": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...", "created": { "utc": "2026-01-15 14:00:00" } } ] } ``` ## Create Signature Signatures are typically captured via the provider mobile app, but can also be created via API. `POST /workorders/{id}/signatures` ```json { "name": "John Doe", "title": "Store Manager", "signature": "data:image/png;base64,..." } ``` ## Delete Signature `DELETE /workorders/{id}/signatures/{signature_id}` > Deleting signatures should only be done when erroneous. They are often required for compliance and audit purposes. ## Signature Requirements You can require signatures as part of the work order configuration: ```json { "requirements": { "signature_required": true, "signature_count": 1 } } ``` ## Signature in Completion When marking work as complete, the system verifies: - Required number of signatures captured - Signature names match expected contacts - Signature timestamp is within service window ## Related - [Tasks](/docs/rest-api/work-orders/execution/tasks) - Task completion requirements - [Contacts](/docs/rest-api/work-orders/communication/contacts) - Site contacts who sign --- ### Tasks URL: /docs/rest-api/work-orders/execution/tasks # Tasks Tasks are a checklist of items the provider must complete. They can be mandatory for closing the work order. ## List Tasks `GET /workorders/{id}/tasks` ### Response ```json { "results": [ { "id": 101, "description": "Take photo of serial number", "group": { "id": "Completion Requirements" }, "label": "Serial Number" }, { "id": 102, "description": "Customer sign-off", "group": { "id": "Verification" }, "label": "Sign-off" } ] } ``` ## Create Task `POST /workorders/{id}/tasks` ```json { "description": "Take a photo of the serial number", "group": "Completion Requirements", "required": true } ``` ## Update Task `PUT /workorders/{id}/tasks/{task_id}` ```json { "description": "Take a photo of BOTH serial numbers", "required": true } ``` ## Delete Task `DELETE /workorders/{id}/tasks/{task_id}` > [INFO] Tasks can only be deleted if they haven't been completed yet. ## Task Completion Providers complete tasks via the mobile app or API: `PUT /workorders/{id}/tasks/{task_id}` ```json { "completed": true } ``` ## Task Alerts Clear alerts for incomplete or overdue tasks. `DELETE /workorders/{id}/tasks/{task_id}/alerts` `DELETE /workorders/{id}/tasks/{task_id}/alerts/{alert_id}` ## Related - [Compliance](/docs/rest-api/work-orders/execution/compliance) - Qualifications and requirements - [Signatures](/docs/rest-api/work-orders/execution/signatures) - Digital signature capture --- ### Time logs URL: /docs/rest-api/work-orders/execution/time-logs # Time Logs Time logs track the actual hours worked by the provider. They can be generated automatically via check-in/check-out or created manually. ## List Time Logs `GET /workorders/{id}/time_logs` ### Response ```json { "results": [ { "id": 1001, "user_id": 12345, "start": { "utc": "2026-01-15 09:00:00" }, "end": { "utc": "2026-01-15 11:30:00" }, "hours": 2.5, "type": "check_in", "verified": true } ], "total_hours": 2.5 } ``` ## Create Time Log `POST /workorders/{id}/time_logs` ```json { "user_id": 12345, "start": { "utc": "2026-01-15 09:00:00" }, "end": { "utc": "2026-01-15 11:00:00" } } ``` ## Update Time Log `PUT /workorders/{id}/time_logs/{time_log_id}` ```json { "out": { "utc": "2026-01-15 12:00:00" } } ``` ## Delete Time Log `DELETE /workorders/{id}/time_logs/{time_log_id}` ## Check-In / Check-Out Providers typically check in and out via the mobile app, which automatically creates time logs. ### Check In `POST /workorders/{id}/check_in` ```json { "user_id": 12345, "location": { "lat": 44.9778, "lng": -93.2650 } } ``` ### Check Out `POST /workorders/{id}/check_out` ```json { "user_id": 12345 } ``` > [INFO] **GPS Verification**: Check-in location is compared against the work order location to verify the provider is on-site. ## Time Log Status | Status | Description | |--------|-------------| | `pending` | Awaiting verification | | `verified` | Confirmed by buyer | | `disputed` | Under review | ## Related - [Tasks](/docs/rest-api/work-orders/execution/tasks) - Work completion checklist - [Financials](/docs/rest-api/work-orders/financials/pay) - Time affects hourly pay --- ### Bonuses penalties URL: /docs/rest-api/work-orders/financials/bonuses-penalties # Bonuses & Penalties You can modify the final payout using Bonuses (incentives) and Penalties (deductions). ## Bonuses Incentives for good performance, urgent work, or special circumstances. ### List Available Bonuses `GET /bonuses` ```json { "results": [ { "id": 1, "name": "Urgent Dispatch", "amount": 25.00 }, { "id": 2, "name": "Weekend Work", "amount": 50.00 }, { "id": 3, "name": "Exceptional Service", "amount": 30.00 } ] } ``` ### List Work Order Bonuses `GET /workorders/{id}/bonuses` ### Apply Bonus `POST /workorders/{id}/bonuses/{bonus_id}` ```json { "amount": 25.00, "description": "Urgent same-day dispatch" } ``` Include bonuses in the initial work order payload: ```json "pay": { "type": "fixed", "base": { "amount": 100 }, "bonuses": { "results": [{ "id": 1 }] } } ``` Add bonus after the work order is assigned: `POST /workorders/{id}/bonuses/1` --- ## Penalties Deductions for late arrivals, missing deliverables, or other issues. ### List Available Penalties `GET /penalties` ```json { "results": [ { "id": 1, "name": "Late Arrival", "amount": 15.00 }, { "id": 2, "name": "Missing Photos", "amount": 10.00 }, { "id": 3, "name": "Incomplete Tasks", "amount": 20.00 } ] } ``` ### List Work Order Penalties `GET /workorders/{id}/penalties` ### Apply Penalty `POST /workorders/{id}/penalties/{penalty_id}` ```json { "amount": 15.00, "reason": "Provider arrived 45 minutes late" } ``` > Penalties can only be applied before the work order is **Paid**. Communicate clearly with providers about deduction reasons. ## Final Pay Calculation ``` Final Pay = Base Pay + Bonuses - Penalties + Approved Expenses ``` ### Example | Component | Amount | |-----------|--------| | Base Pay (Fixed) | $150.00 | | + Urgent Bonus | $25.00 | | - Late Penalty | -$15.00 | | + Materials Expense | $45.00 | | **Total** | **$205.00** | ## Related - [Pay](/docs/rest-api/work-orders/financials/pay) - Base pay structure - [Expenses](/docs/rest-api/work-orders/financials/expenses) - Reimbursable costs - [Company](/docs/rest-api/resources/company) - Configure bonus/penalty types --- ### Discounts increases URL: /docs/rest-api/work-orders/financials/discounts-increases # Discounts & Increases Beyond bonuses and penalties, work orders can have **Discounts** (buyer-side reductions) and **Increases** (pay adjustments). ## Discounts Discounts reduce the buyer's cost without affecting provider pay. ### List Discounts `GET /workorders/{work_order_id}/discounts` ```json { "results": [ { "id": 101, "type": "percentage", "value": 10, "description": "Volume discount", "applied_by": "system", "created": { "utc": "2026-01-15 10:00:00" } }, { "id": 102, "type": "fixed", "value": 25.00, "description": "Promotional discount", "applied_by": "admin", "created": { "utc": "2026-01-15 10:00:00" } } ], "total_discount": 40.00 } ``` ### Discount Types | Type | Description | Example | |------|-------------|---------| | `percentage` | Percentage off total | 10% off = $15 on $150 job | | `fixed` | Fixed dollar amount | $25 off | > [INFO] Discounts are typically applied through business rules or admin actions, not directly via API in most integrations. --- ## Pay Increases Pay increases adjust the provider's compensation, often due to scope changes or negotiations. ### List Increases `GET /workorders/{work_order_id}/increases` ```json { "results": [ { "id": 201, "amount": 50.00, "reason": "Additional equipment needed", "status": "approved", "requested_by": "provider", "approved_by": { "id": 500, "name": "Dispatch Manager" }, "created": { "utc": "2026-01-15 14:00:00" } } ], "total_increase": 50.00 } ``` ### Get Specific Increase `GET /workorders/{work_order_id}/increases/{increase_id}` ### Update Increase `PUT /workorders/{work_order_id}/increases/{increase_id}` ```json { "status": "approved" } ``` ```json { "status": "rejected", "rejection_reason": "Outside scope agreement" } ``` ```json { "amount": 35.00, "status": "approved", "note": "Approved partial increase" } ``` ## Increase Status Flow ```mermaid stateDiagram-v2 [*] --> Pending: Provider requests Pending --> Approved: Buyer approves Pending --> Rejected: Buyer rejects Pending --> Modified: Buyer adjusts amount Modified --> Approved: Auto-approved Approved --> [*]: Added to final pay Rejected --> [*]: Not applied ``` ## Common Increase Reasons | Reason | Description | |--------|-------------| | Scope change | Work requirements expanded | | Additional equipment | Extra hardware needed | | Extended time | Job took longer than expected | | Travel adjustment | Further distance than quoted | | Emergency/urgent | Rush job premium | ## Final Pay Calculation ``` Final Pay = Base Pay + Bonuses - Penalties + Approved Increases + Approved Expenses Buyer Cost = Final Pay - Discounts + Platform Fees ``` > Increases can only be approved before the work order reaches **Paid** status. ## Related - [Pay](/docs/rest-api/work-orders/financials/pay) - Base pay structure - [Bonuses & Penalties](/docs/rest-api/work-orders/financials/bonuses-penalties) - Standard incentives/deductions - [Expenses](/docs/rest-api/work-orders/financials/expenses) - Reimbursable costs --- ### Expenses URL: /docs/rest-api/work-orders/financials/expenses # Expenses Providers can request reimbursement for materials, travel, and other costs incurred during work. You must review and approve these expenses before the work order can be paid. ## List Expenses `GET /workorders/{id}/expenses` ### Response ```json { "results": [ { "id": 501, "category": { "id": 1, "name": "Materials" }, "amount": 45.00, "description": "Cat6 cable - 100ft", "receipt_url": "https://cdn.fieldnation.com/receipts/abc.webp", "status": "pending", "created": { "utc": "2026-01-15 15:00:00" } } ], "totals": { "pending": 45.00, "approved": 0.00, "rejected": 0.00 } } ``` ## Create Expense `POST /workorders/{id}/expenses` ```json { "category": { "id": 1 }, "amount": 45.00, "description": "Cat6 cable - 100ft", "quantity": 1 } ``` ## Update Expense `PUT /workorders/{id}/expenses/{expense_id}` ```json { "status": "approved" } ``` ```json { "status": "rejected", "rejection_reason": "Receipt not legible" } ``` ```json { "amount": 40.00, "status": "approved" } ``` ## Expense Categories Get available expense categories for your company: `GET /company/{company_id}/expenses` ```json { "results": [ { "id": 1, "name": "Materials", "requires_receipt": true }, { "id": 2, "name": "Mileage", "requires_receipt": false }, { "id": 3, "name": "Parking", "requires_receipt": true } ] } ``` > [INFO] Expenses requiring receipts cannot be approved unless a receipt image is attached. ## Expense Status Flow ``` Pending → Approved → Included in Payment ↘ Rejected (with reason) ``` ## Related - [Pay](/docs/rest-api/work-orders/financials/pay) - Base pay structure - [Company](/docs/rest-api/resources/company) - Expense categories --- ### Pay URL: /docs/rest-api/work-orders/financials/pay # Pay Structure The **Pay** object controls the payment terms for a work order. ## Pay Types Field Nation supports two primary pay models: > [INFO] **Choosing a Model:** - **Fixed Pay**: Best for well-defined tasks (e.g., "Replace 1 Screen"). Easier budgeting. - **Hourly Pay**: Best for diagnostics or uncertain scopes. Prevents friction if the job takes longer than expected. A flat rate for the entire job, regardless of time spent. ```json "pay": { "type": "fixed", "base": { "amount": 150.00, "units": 1 } } ``` - `amount`: Total pay in dollars - `units`: Always 1 for fixed pay A rate per hour, up to a maximum number of hours (capped). ```json "pay": { "type": "hourly", "base": { "amount": 65.00, "units": 4 } } ``` - `amount`: Rate per hour - `units`: Maximum hours (cap) ## Get Pay Details `GET /workorders/{id}/pay` ### Response ```json { "type": "hourly", "base": { "amount": 65.00, "units": 4 }, "total": { "amount": 195.00, "hours_worked": 3.0 }, "bonuses": [], "penalties": [], "expenses": { "pending": 25.00, "approved": 0.00 } } ``` ## Update Pay `PUT /workorders/{id}` ```json { "pay": { "type": "fixed", "base": { "amount": 200.00 } } } ``` > Pay can only be updated before the work order reaches **Work Done** status. ## Pay Visibility | Viewer | Can See | |--------|---------| | Buyer | Full pay breakdown | | Provider | Their rate + earned amount | | Marketplace | Displayed rate for requests | ## Pay Calculation The final pay is calculated as: ``` Final Pay = Base Pay + Bonuses - Penalties + Approved Expenses ``` For **hourly pay**, base pay is calculated as: ``` Base Pay = min(hours_worked, max_hours) × hourly_rate ``` ## Related - [Expenses](/docs/rest-api/work-orders/financials/expenses) - Reimbursable costs - [Bonuses & Penalties](/docs/rest-api/work-orders/financials/bonuses-penalties) - Incentives and deductions - [Time Logs](/docs/rest-api/work-orders/execution/time-logs) - Hours affect hourly pay - [Create Work Order](/docs/rest-api/work-orders/basics/create) - Setting pay at creation --- ### Bundles URL: /docs/rest-api/work-orders/logistics/bundles # Bundles Bundles allow you to group multiple work orders together so a single provider can accept them all at once. This is useful for multi-site rollouts in a small geographic area. ## Create Bundle `POST /workorders/bundle` ```json { "work_order_ids": [1001, 1002, 1003] } ``` ### Response ```json { "bundle_id": "BND-12345", "work_orders": [ { "id": 1001, "title": "Site A Install" }, { "id": 1002, "title": "Site B Install" }, { "id": 1003, "title": "Site C Install" } ], "total_pay": 450.00 } ``` > [INFO] **Note**: Bundling must be done *before* assignment. You cannot bundle already-assigned work orders. ## Get Bundle Details `GET /workorders/{id}/bundle` ## Unbundle Work Orders Remove work orders from a bundle: `POST /workorders/unbundle` ```json { "work_order_ids": [1002] } ``` ## Bundle Rules | Rule | Description | |------|-------------| | Pre-assignment only | Can only bundle before providers are assigned | | Same geographic area | Bundle WOs in reasonable proximity | | Compatible schedules | Service windows should align | | Single provider | All bundled WOs go to same provider | ## Bundle Benefits 1. **Efficiency**: Provider accepts multiple jobs at once 2. **Travel optimization**: Nearby sites can be done in sequence 3. **Pay clarity**: Combined pay shown upfront 4. **Single routing**: Route bundle to talent pool once ## Unbundling Scenarios - Scope changes requiring different skills - Schedule conflicts - Provider requests removal of one site > Unbundling after routing may require re-routing the removed work orders. ## Related - [Workflow](/docs/rest-api/work-orders/workflow/lifecycle) - Routing bundles - [Schedule](/docs/rest-api/work-orders/logistics/schedule) - Coordinating bundle timing --- ### Revisits URL: /docs/rest-api/work-orders/logistics/revisits # Revisits When an incident or incomplete status requires the provider to return, a **Revisit** is created. ## List Revisits `GET /workorders/{id}/site-revisits` ### Response ```json { "results": [ { "id": 301, "reason": { "id": 10, "name": "Parts not delivered" }, "notes": "Returning with correct router model", "scheduled": { "utc": "2026-01-17 09:00:00" }, "status": "scheduled", "created": { "utc": "2026-01-15 16:00:00" } } ], "count": 1 } ``` ## Create Revisit `POST /workorders/{id}/site-revisits` ```json { "revisit_reason_id": 10, "notes": "Returning with correct parts.", "scheduled": { "utc": "2026-01-17 09:00:00" } } ``` ## Revisit Reasons Get available revisit reasons for your company: `GET /company/{company_id}/reasons/revisit` ```json { "results": [ { "id": 10, "name": "Parts not delivered" }, { "id": 11, "name": "Customer requested callback" }, { "id": 12, "name": "Scope change" }, { "id": 13, "name": "Site not ready" } ] } ``` ## Incidents An **Incident** flags that something went wrong (e.g., "Equipment failure"). It is often the reason *why* a revisit is needed. ### Set Incident `PUT /workorders/{id}/incident` ```json { "incident_id": 12 } ``` ## Job Status Track the current state of work: `GET /workorders/{id}/job-status` ```json { "status": "incomplete", "reason": "Parts missing", "revisit_scheduled": true } ``` ## Revisit Flow ```mermaid flowchart TD A[Work In Progress] --> B{Issue Discovered?} B -->|Yes| C[Create Incident] C --> D[Mark Incomplete] D --> E[Create Revisit] E --> F[Schedule Return Visit] F --> G[Provider Returns] G --> H{Work Complete?} H -->|Yes| I[Approve & Pay] H -->|No| E B -->|No| J[Complete Work] J --> I ``` > [INFO] Revisits extend the work order lifecycle. Pay may be adjusted based on revisit circumstances. ## Related - [Schedule](/docs/rest-api/work-orders/logistics/schedule) - Scheduling return visits - [Compliance](/docs/rest-api/work-orders/execution/compliance) - Problems and issues --- ### Schedule URL: /docs/rest-api/work-orders/logistics/schedule # Schedule & ETA Manage the when and timing of work orders. ## Update Schedule `PUT /workorders/{id}/schedule` Provider must arrive at a specific time: ```json { "service_window": { "mode": "exact", "start": { "utc": "2026-01-15 09:00:00" } } } ``` Provider can arrive anytime within a window: ```json { "service_window": { "mode": "between", "start": { "utc": "2026-01-15 09:00:00" }, "end": { "utc": "2026-01-15 17:00:00" } } } ``` ## Schedule Modes | Mode | Description | |------|-------------| | `exact` | Must arrive at specific time | | `between` | Arrival window (e.g., 9am-5pm) | | `asap` | As soon as possible | ## Provider ETA Providers signal their expected arrival time: `POST /workorders/{id}/eta` ```json { "eta": { "utc": "2026-01-15 09:30:00" }, "notes": "Traffic is light, arriving early" } ``` ## Get Schedule Details `GET /workorders/{id}/schedule` ```json { "service_window": { "mode": "between", "start": { "utc": "2026-01-15 09:00:00" }, "end": { "utc": "2026-01-15 17:00:00" } }, "provider_eta": { "utc": "2026-01-15 10:00:00" }, "time_zone": "America/Chicago" } ``` > [INFO] All times are in UTC. Convert to local time zone for display using the `time_zone` field. ## Related - [Revisits](/docs/rest-api/work-orders/logistics/revisits) - Return visits - [Workflow](/docs/rest-api/work-orders/workflow/lifecycle) - Check-in/check-out --- ### Shipments URL: /docs/rest-api/work-orders/logistics/shipments # Shipments If you are shipping parts to the site (or expecting a return), use the Shipments object to track deliveries. > **Why use Shipments?** Adding tracking numbers here allows the provider to see shipment status directly in their mobile app. This significantly reduces "Where is the part?" phone calls. ## List Shipments `GET /workorders/{id}/shipments` ### Response ```json { "results": [ { "id": 201, "description": "Router Box", "carrier": "FedEx", "tracking_number": "123456789", "direction": "to_site", "status": "delivered", "created": { "utc": "2026-01-10 10:00:00" } } ] } ``` ## Create Shipment `POST /workorders/{id}/shipments` ```json { "description": "Router Box", "carrier": { "name": "FedEx", "tracking": "123456789" }, "direction": "to_site" } ``` ## Update Shipment `PUT /workorders/{id}/shipments/{shipment_id}` ```json { "status": "delivered", "tracking_number": "123456789-updated" } ``` ## Delete Shipment `DELETE /workorders/{id}/shipments/{shipment_id}` ## Shipment Direction | Direction | Description | |-----------|-------------| | `to site` | Parts shipping TO the work location | | `from site` | Returns shipping FROM the site | | `to provider` | Parts shipping directly to the provider | | `to other location` | Shipping to a third-party location | ## Carriers Supported carrier values (case-insensitive name): - `ups` - `fedex` - `usps` - `other` ## Shipment Status | Status | Description | |--------|-------------| | `pending` | Shipment created, not yet sent | | `in_transit` | Package is on the way | | `delivered` | Package arrived | | `delayed` | Shipping issues | > Link tracking numbers so providers can check delivery status before traveling to the site. ## Related - [Schedule](/docs/rest-api/work-orders/logistics/schedule) - Coordinate timing with deliveries - [Bundles](/docs/rest-api/work-orders/logistics/bundles) - Multi-site shipments --- ### Assignments URL: /docs/rest-api/work-orders/workflow/assignments # Assignments When you are ready to commit to a provider, you **Assign** them. This is the contract step. > **Binding Contract**: Assigning a provider creates a financial obligation (funds are held or verified) and a legal agreement to perform the work. ## Strategies - **Direct Assignment**: You know the provider (`user_id`). - **From Request**: You routed the work, and are accepting a provider's application. ## Assignment Flow ```mermaid sequenceDiagram participant B as Buyer participant WO as Work Order participant P as Provider B->>WO: Route to providers P->>WO: Request work (optional counter-offer) B->>WO: Review requests alt Accept Request B->>WO: POST /assignee WO->>P: Assignment confirmed else Counter-Offer P->>WO: Submit counter-offer B->>WO: Accept/Reject offer else Reject B->>WO: DELETE request B->>WO: Route to others end ``` ## Assign a Provider `POST /workorders/{id}/assignee?clientPayTermsAccepted=true` ```json { "user": { "id": 12345 } } ``` ### Assignment Response ```json { "id": 1001, "status": "Assigned", "assignee": { "id": 12345, "first_name": "John", "last_name": "Smith", "rating": 4.8 } } ``` ## Unassign a Provider Remove the current provider from the work order. `DELETE /workorders/{id}/assignee` > Unassigning after the provider has started work may require providing a reason and could affect your company's metrics. ## Provider Requests When you route a work order, providers can **Request** it. You can view and manage these requests. ### List Requests `GET /workorders/{id}/requests` ```json { "results": [ { "id": 501, "user": { "id": 12345, "name": "John Smith" }, "status": "pending", "counter_offer": null, "created": { "utc": "2026-01-15 10:00:00" } } ] } ``` ### Accept a Request `POST /workorders/{id}/requests/{request_id}/accept` ### Deny a Request `DELETE /workorders/{id}/requests/{request_id}` ## Counter-Offers Providers can submit a counter-offer with different pay terms. ```json { "id": 501, "user": { "id": 12345 }, "counter_offer": { "pay": { "type": "fixed", "base": { "amount": 175.00 } }, "note": "Travel distance is significant" } } ``` `POST /workorders/{id}/requests/{request_id}/accept` Accepting a counter-offer assigns the provider at their requested rate. `DELETE /workorders/{id}/requests/{request_id}` You can reject and continue routing to other providers. ## Assignment History View all past assignments for a work order. `GET /workorders/{id}/assignments` ```json { "results": [ { "id": 1, "user": { "id": 12345, "name": "John Smith" }, "status": "completed", "assigned_at": { "utc": "2026-01-15 10:00:00" } } ] } ``` ## Related - [Workflow Lifecycle](/docs/rest-api/work-orders/workflow/lifecycle) - Full state management - [Providers](/docs/rest-api/resources/providers) - Finding providers --- ### Lifecycle URL: /docs/rest-api/work-orders/workflow/lifecycle # Workflow Lifecycle Once a work order is created, you must move it through its lifecycle using specific API actions. ## State Diagram ```mermaid stateDiagram-v2 [*] --> Draft: Create Draft --> Published: Publish Published --> Routed: Route Routed --> Assigned: Assign Assigned --> InProgress: Check-in InProgress --> WorkDone: Check-out WorkDone --> Approved: Approve WorkDone --> Incomplete: Reject Incomplete --> InProgress: Revisit Approved --> Paid: Payment Paid --> [*] ``` ## 1. Publish A work order starts in `Draft` state. It is invisible to anyone but you. **Publishing** changes the state to `Open` (or `Routed`), making it visible to the providers you route it to. `POST /workorders/{id}/publish` > [INFO] You can also unpublish a work order to return it to Draft state using `DELETE /workorders/{id}/publish`. ## 2. Route vs. Assign You have two primary ways to fill a work order: 1. **Route** (Marketplace/Network): Invite multiple providers to request the work. You review requests and select the best fit. 2. **Assign** (Direct): You already know who you want. You bypass the request phase and directly contract a specific provider. > Use **Route** when you need to find talent or get competitive offers. Use **Assign** when you have a dedicated W-2 employee or a preferred contractor you've already spoken with. ### Routing Routing sends the work order to specific providers or groups, inviting them to request the work. `POST /workorders/{id}/route` ### Routing to a Provider ```json { "technician": { "id": 12345 } } ``` ### Mass Routing To route to many providers at once (e.g., a Talent Pool), use the mass-route endpoint. `POST /workorders/{id}/mass-route` ```json { "talent_pool_id": 9921 } ``` ### Auto Dispatch Automatically routes the work order based on pre-defined rules. `POST /workorders/auto_dispatch` ### Selection Rules When routing, you can apply **Selection Rules** to filter providers. ```json { "selection_rule": { "rules": [ { "name": "rating", "operator": ">=", "value": 4.5 } ] } } ``` ## 3. Assign See [Assignments](/docs/rest-api/work-orders/workflow/assignments) for detailed assignment management. `POST /workorders/{id}/assignee` ## 4. Closing the Loop ### Approve If satisfied with the work, approve to release payment. `POST /workorders/{id}/approve` ### Incomplete If unsatisfied, mark it incomplete to send it back. `DELETE /workorders/{id}/complete` **Required Params**: - `reason`: Text description - `reason_id`: The ID of the incomplete reason Use `GET /company/{id}/reasons/incomplete` to get valid Reason IDs for your company. ## API Quick Reference | Action | Endpoint | Method | |--------|----------|--------| | Publish | `/workorders/{id}/publish` | POST | | Unpublish | `/workorders/{id}/publish` | DELETE | | Route to provider | `/workorders/{id}/route` | POST | | Mass route | `/workorders/{id}/mass-route` | POST | | Auto dispatch | `/workorders/auto_dispatch` | POST | | Assign | `/workorders/{id}/assignee` | POST | | Approve | `/workorders/{id}/approve` | POST | | Mark incomplete | `/workorders/{id}/complete` | DELETE | ## Related - [Assignments](/docs/rest-api/work-orders/workflow/assignments) - Detailed assignment management - [Create Work Order](/docs/rest-api/work-orders/basics/create) - Creating work orders - [Revisits](/docs/rest-api/work-orders/logistics/revisits) - Handling incomplete work - [Company Settings](/docs/rest-api/resources/company) - Incomplete reasons --- ### Provider actions URL: /docs/rest-api/work-orders/workflow/provider-actions # Provider Actions Providers may need to cancel or report delays. These endpoints handle those lifecycle exceptions. ## Provider Cancel A provider can request to be removed from a work order. `POST /workorders/{id}/provider/cancel` ```json { "reason": "Vehicle breakdown", "reason_id": 5, "note": "Cannot make it to site today" } ``` This will: 1. Remove the provider assignment 2. Change work order status back to **Routed** (or Published) 3. Log the cancellation reason ## Provider Delay A provider can signal they will be late. `POST /workorders/{id}/provider/delay` ```json { "delay_minutes": 30, "reason": "Traffic accident ahead", "new_eta": { "utc": "2026-01-15 10:30:00" } } ``` ## Get Routed Providers List all providers who have currently been routed the work order (visible to them). `GET /workorders/{id}/providers` ```json { "results": [ { "id": 12345, "first_name": "John", "last_name": "Doe", "status": "requesting" }, { "id": 67890, "first_name": "Jane", "last_name": "Smith", "status": "routed" } ] } ``` ## Related - [Assignments](/docs/rest-api/work-orders/workflow/assignments) - Managing assignments - [Schedule](/docs/rest-api/work-orders/logistics/schedule) - Updating ETAs - [Revisits](/docs/rest-api/work-orders/logistics/revisits) - Rescheduling work --- ### Smart dispatch URL: /docs/rest-api/work-orders/workflow/smart-dispatch # Smart Dispatch Smart Dispatch automates the process of finding and assigning providers based on preset logic. ## Trigger Smart Dispatch Manually trigger the Smart Dispatch logic for a work order. `POST /workorders/smart-dispatch` ```json { "work_order_ids": [1001, 1002, 1003], "strategy": "standard" } ``` This will evaluate the work orders against your active routing rules and dispatch them accordingly. --- ## Project Dispatch Settings Configure dispatch automation rules at the **Project** level. ### Get Dispatch Settings `GET /project/{project_id}/dispatch-settings` ```json { "mode": "automatic", "rules": [ { "order": 1, "type": "preferred_group", "target_id": 500, "delay_hours": 0 }, { "order": 2, "type": "marketplace", "criteria": { "rating": 4.5 }, "delay_hours": 24 } ] } ``` ### Create/Update Settings `POST /project/dispatch-settings` or `PUT /project/{project_id}/dispatch-settings` ```json { "project_id": 123, "mode": "automatic", "rules": [ { "order": 1, "type": "talent_pool", "target_id": 99, "wait_time": 4 } ] } ``` ## How It Works ```mermaid graph TD A[Work Order Created] --> B{Project has rules?} B -->|Yes| C[Evaluate Rules] B -->|No| D[Manual Routing Needed] C --> E[Rule 1: Route to Preferred] E --> F[Wait 4 Hours] F --> G{Accepted?} G -->|No| H[Rule 2: Route to Marketplace] G -->|Yes| I[Assigned] ``` ## Related - [Lifecycle](/docs/rest-api/work-orders/workflow/lifecycle) - Manual routing methods - [Projects](/docs/rest-api/resources/projects) - Project configuration - [Talent Pools](/docs/rest-api/resources/talent-pools) - Targets for dispatch --- ## Webhooks Overview (22) ### Api playground URL: /docs/webhooks/api-playground Visit the Webhooks v3 playground to test payloads, signatures, and responses: --- ### Introduction URL: /docs/webhooks/introduction ## What Are Webhooks? Webhooks are **server-to-server HTTP callbacks** that notify your system when specific events happen in Field Nation. Think of them as "reverse API calls" – instead of your system asking Field Nation for updates, Field Nation pushes updates to you. ```mermaid sequenceDiagram participant FN as Field Nation participant Your as Your System Note over FN: Work Order Published FN->>Your: POST https://your-endpoint.com/webhooks Note over Your: Process Event Your-->>FN: 200 OK Note over Your: Update Local Records ``` ### How They Work 1. **Subscribe** - Register your HTTPS endpoint and select events to receive 2. **Event Occurs** - A work order is created, updated, or status changes 3. **Notification Sent** - Field Nation POSTs event payload to your endpoint 4. **Process & Respond** - Your system processes the event and returns 200 OK 5. **Automatic Retry** - If delivery fails, Field Nation retries with exponential backoff --- ## Why Use Webhooks? Receive updates within seconds of events occurring. Keep your systems in sync without polling delays. Eliminate constant polling. Webhooks push data only when something changes, reducing API calls by 95%+. Build reactive workflows that trigger automatically. Assign providers, update ERP systems, send notifications—all in real-time. Lower infrastructure costs. No need to run continuous polling jobs or maintain complex scheduling logic. --- ## Common Use Cases ### 1. **Work Order Lifecycle Automation** Automatically respond to work order status changes: - **Published** → Auto-assign to preferred provider - **Assigned** → Update dispatch board, notify technician - **Checked In** → Log start time in ERP system - **Work Done** → Trigger approval workflow - **Approved** → Generate invoice, update accounting system ### 2. **Real-Time Notifications** Keep stakeholders informed instantly: - Notify clients when work orders are completed - Alert managers when providers decline assignments - Send SMS/email when work is approved - Update dashboards in real-time ### 3. **Data Synchronization** Maintain consistent data across systems: - Sync work order details to Salesforce, ServiceNow, or NetSuite - Update custom dashboards and reporting tools - Mirror Field Nation data in your database - Replicate changes to multiple downstream systems ### 4. **Compliance & Auditing** Track every change for regulatory requirements: - Log all status transitions with timestamps - Record who approved, declined, or modified work orders - Maintain audit trails for compliance reporting - Alert on specific event patterns --- ## When to Use Webhooks vs REST API | Scenario | Use Webhooks | Use REST API | |----------|-------------|--------------| | **Real-time updates** | ✅ Ideal | ❌ Requires polling | | **Event-driven workflows** | ✅ Perfect fit | ⚠️ Complex to implement | | **Creating work orders** | ❌ Not supported | ✅ Use POST requests | | **Bulk data queries** | ❌ Not designed for this | ✅ Use GET endpoints | | **On-demand data retrieval** | ❌ Event-based only | ✅ Query anytime | | **Reducing API calls** | ✅ Push notifications | ❌ Constant polling | > [INFO] **Best Practice**: Use webhooks for event-driven updates and REST API for on-demand queries and write operations. Most integrations use both together. --- ## Available Events Field Nation Webhooks support **33 distinct events** covering the complete work order lifecycle: ### Event Categories - **workorder.created** - New work order created - **workorder.routed** - Routed to specific provider(s) - **workorder.requested** - Provider requested assignment - **workorder.declined** - Provider declined assignment - **workorder.undeclined** - Decline reversed - **workorder.status.published** - Work order published to marketplace - **workorder.status.assigned** - Provider assigned to work order - **workorder.status.on_my_way** - Provider en route - **workorder.status.checked_in** - Provider arrived on site - **workorder.status.work_done** - Work completed - **workorder.status.approved** - Work approved by buyer - And 14 more status changes... - **workorder.message_posted** - New message or reply - **workorder.provider_upload** - Document uploaded - **workorder.task_completed** - Task marked complete - **workorder.schedule_updated** - Schedule changed - **workorder.tag_added** / **tag_removed** - Tags modified [View complete event catalog →](/docs/webhooks/concepts/events) --- ## Key Features ### 🔒 **Secure Delivery** - **HMAC-SHA256 signatures** - Verify webhook authenticity - **Custom headers** - Pass authentication tokens - **IP whitelisting** - Restrict to Field Nation IPs - **HTTPS required** - Encrypted transmission [Learn about security →](/docs/webhooks/guides/security) ### 🔁 **Automatic Retries** - **Exponential backoff** - 10s, 20s, 40s, 80s, 160s, 320s, 640s - **Up to 7 attempts** - Based on delivery success rate - **Manual retry** - Reprocess failed deliveries via API - **Dead letter queue** - Access failed events for debugging [Understanding delivery →](/docs/webhooks/concepts/delivery) ### 📊 **Comprehensive Monitoring** - **Delivery logs** - Every attempt recorded - **Pre-signed log URLs** - Access complete request/response - **Filtering & search** - Find deliveries by event, status, work order - **Change history** - Audit trail of webhook modifications [Monitoring webhooks →](/docs/webhooks/guides/monitoring) ### 🛠️ **Flexible Management** - **Web UI** - Visual webhook configuration dashboard - **REST API** - Programmatic webhook management - **Multiple webhooks** - Different endpoints per event type - **Status control** - Active, inactive, or archived [Creating webhooks →](/docs/webhooks/guides/creating-webhooks) --- ## Prerequisites Before working with webhooks, ensure you have: ### Field Nation Account Access - **Buyer account** with active contract - **API credentials** (client_id and client_secret) - **Webhook access** enabled (contact support if needed) ### Technical Requirements - **HTTPS endpoint** - Publicly accessible URL - **Response within 5 seconds** - Acknowledge deliveries quickly - **2xx status code** - Indicate successful receipt ### Development Tools (Optional) - **ngrok** or **localtunnel** - For local testing - **Request inspection tools** - webhook.site, request.bin - **Monitoring setup** - Logs, alerts, dashboards [Complete prerequisites guide →](/docs/getting-started/prerequisites) --- ## Getting Started Ready to receive your first webhook? Create your first webhook and receive events in under 15 minutes Understand events, payloads, and delivery mechanics Verify signatures and secure your webhook endpoint Transform payloads with JSONata — filter fields, rename keys, and build dynamic URLs without middleware Complete API documentation for programmatic management --- ## Need Help? > [INFO] **Support Resources** - **Technical Issues**: [Submit a support case](https://app.fieldnation.com/support-cases) - **Email**: integrations-engineering@fieldnation.com - **API Playground**: [Swagger UI (Sandbox)](https://ui-sandbox.fndev.net/integrations/webhooks/_api) --- --- ### Migration URL: /docs/webhooks/migration ## 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 Name | v3 Event Name | Notes | |---------------|---------------|-------| | `work_order.created` | `workorder.created` | Removed underscore | | `work_order.published` | `workorder.status.published` | Now a status change event | | `work_order.assigned` | `workorder.status.assigned` | Now a status change event | | `work_order.completed` | `workorder.status.work_done` | Renamed to match platform terminology | | `work_order.approved` | `workorder.status.approved` | Now 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):** ```json { "id": "evt_123", "event": "work_order.published", "work_order_id": 12345, "created_at": "2026-01-15T10:00:00Z", "work_order": { // work order data } } ``` **v3 Payload (standardized):** ```json { "eventId": "evt_abc123", "eventName": "workorder.status.published", "workOrderId": 12345, "timestamp": "2026-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):** ```javascript // Authorization: Basic base64(username:password) const auth = req.headers.authorization; const [username, password] = Buffer.from(auth.split(' ')[1], 'base64') .toString() .split(':'); ``` **v3 (HMAC-SHA256):** ```javascript // 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 →](/docs/webhooks/guides/security) --- ## Migration Steps ### Audit Current Webhook Usage Document your current webhooks: ```javascript // 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: ```javascript 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: ```javascript 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: ```javascript 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: ```bash # 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: ```javascript // 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: ```javascript 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: ```javascript // 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: ```javascript // 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: ```javascript 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: ```javascript // 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: ```javascript // 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](https://app.fieldnation.com/support-cases) - **API Documentation**: [developers.fieldnation.com/docs/webhooks](https://developers.fieldnation.com/docs/webhooks/introduction) --- --- ### Quickstart URL: /docs/webhooks/quickstart ## Overview By the end of this quickstart, you'll have: - ✅ A publicly accessible webhook endpoint - ✅ A webhook configured in Field Nation - ✅ Received and verified a real webhook event - ✅ Validated the HMAC-SHA256 signature **Estimated Time**: 10-15 minutes --- ## Step 1: Set Up Your Endpoint You need a publicly accessible HTTPS endpoint to receive webhooks. Choose your approach: **Perfect for testing** - No code required! ### Using webhook.site 1. Visit [webhook.site](https://webhook.site) 2. Copy your unique URL (e.g., `https://webhook.site/abc123...`) 3. Keep the tab open to see incoming requests ```plaintext Your Webhook URL: https://webhook.site/abc-1234-def-5678 ``` > [INFO] **Tip**: webhook.site automatically returns 200 OK and displays request details. Perfect for initial testing! **Test with your local server** ### Install ngrok ```bash # macOS (Homebrew) brew install ngrok/ngrok/ngrok # Or download from https://ngrok.com/download ``` ### Start your local server ```javascript title="server.js" const 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'); }); ``` ### Expose with ngrok ```bash ngrok http 3000 ``` Copy the HTTPS forwarding URL: ```plaintext Forwarding: https://abc123.ngrok-free.app → localhost:3000 ``` Your webhook URL: `https://abc123.ngrok-free.app/webhooks/fieldnation` **For production use** ### Example Express.js Server ```javascript title="webhook-server.js" 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.) --- ## Step 2: Get API Credentials You need an OAuth access token to create webhooks via API. ### Obtain Credentials If you don't have them yet: 1. Navigate to [Field Nation Support](https://app.fieldnation.com/support-cases) 2. Submit a case requesting API credentials 3. Receive `client_id`, `client_secret`, `username`, and `password` ### Generate Access Token ```bash title="Get OAuth Token" curl -X POST "https://api-sandbox.fndev.net/authentication/api/oauth/token" \ -H "Content-Type: application/json" \ -d '{ "grant_type": "password", "client_id": "YOUR_CLIENT_ID", "client_secret": "YOUR_CLIENT_SECRET", "username": "YOUR_USERNAME", "password": "YOUR_PASSWORD" }' ``` Response: ```json { "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. --- ## Step 3: Create Your Webhook Choose your preferred method: ### Using the Webhooks Dashboard ### Navigate to Dashboard - **Sandbox**: [https://ui-sandbox.fndev.net/integrations/webhooks](https://ui-sandbox.fndev.net/integrations/webhooks) - **Production**: [https://app.fieldnation.com/integrations/webhooks](https://app.fieldnation.com/integrations/webhooks) ### Click "Create New" ### Configure Basic Settings - **URL**: Your endpoint from Step 1 - **HTTP Method**: POST (recommended) - **Status**: Active ### Select Events For this quickstart, select: - `workorder.created` - `workorder.status.published` - `workorder.status.assigned` > [INFO] **Tip**: Start with a few events, then expand once you're comfortable. ### Save Webhook Configuration Note your **Webhook ID** and **Secret** - you'll need these for signature verification. ### Using the Webhooks API ```bash title="Create Webhook" 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:** ```json { "metadata": { "timestamp": "2026-03-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": "2026-03-15T10:30:00Z" } } ``` > **Save the secret!** You'll use this to verify webhook signatures. It's only shown once during creation. --- ## Step 4: Trigger a Test Event Now let's create an event to trigger your webhook: ### Create a Work Order (Sandbox) Use the Field Nation sandbox UI or API to create a test work order: **Via Sandbox UI:** 1. Login to [Sandbox](https://ui-sandbox.fndev.net) 2. Navigate to Work Orders 3. Click "Create Work Order" 4. Fill in basic details and publish **Via REST API:** ```bash curl -X POST "https://api-sandbox.fndev.net/api/rest/v2/workorder?access_token=YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "title": "Test Webhook Work Order", "description": "Testing webhook delivery", "schedule": { "serviceWindow": { "start": "2026-06-20T09:00:00Z", "end": "2026-06-20T17:00:00Z" } } }' ``` > [INFO] The REST API (v2) authenticates via query parameter (`?access_token=TOKEN`), while the Webhooks API (v1) uses the `Authorization: Bearer` header. They use different auth methods. ### Check Your Endpoint Within seconds, you should receive a `workorder.created` event: ```json { "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" } } } ``` --- ## Step 5: Verify Webhook Signature **Security critical!** Always verify that webhooks actually came from Field Nation: ```javascript 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'); }); ``` ```python 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 eventName); http_response_code(200); echo 'OK'; ?> ``` ```go 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. --- ## Step 6: Monitor Delivery Check that your webhook was delivered successfully: ### Via Web UI 1. Navigate to [Webhooks Dashboard](https://ui-sandbox.fndev.net/integrations/webhooks) 2. Click on your webhook 3. Go to "Delivery Logs" tab 4. Verify delivery status is 200 ### Via API ```bash curl -X GET "https://api-sandbox.fndev.net/api/v1/webhooks/delivery-logs?webhookId=YOUR_WEBHOOK_ID" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` **Response:** ```json { "metadata": { "timestamp": "2026-03-15T10:40:00Z", "count": 1, "total": 1 }, "result": [ { "deliveryId": "del_xyz789", "webhookId": "wh_abc123", "eventName": "workorder.created", "deliveryStatus": 200, "deliveryAttempt": 1, "createdAt": "2026-03-15T10:35:05Z" } ] } ``` --- ## Troubleshooting ### Not Receiving Webhooks? Ensure your webhook is **active**, not inactive or archived. ```bash curl -X GET https://api-sandbox.fndev.net/api/v1/webhooks/YOUR_WEBHOOK_ID \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` Test your endpoint is publicly accessible: ```bash curl -X POST https://your-endpoint.com/webhooks \ -H "Content-Type: application/json" \ -d '{"test": "data"}' ``` Should return 200 OK. Look for failed deliveries and error messages: - **404 Not Found**: Check your URL path - **SSL Error**: Verify HTTPS certificate is valid - **Timeout**: Ensure response within 5 seconds Confirm you're subscribed to the event you triggered: ```bash # List subscribed events curl -X GET https://api-sandbox.fndev.net/api/v1/webhooks/YOUR_WEBHOOK_ID \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ | jq '.result.events' ``` ### Common Issues | 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 →](/docs/webhooks/troubleshooting/common-issues) --- --- ### Delivery logs URL: /docs/webhooks/api-reference/delivery-logs ## Endpoints Overview | Method | Endpoint | Description | |--------|----------|-------------| | **GET** | `/api/v1/webhooks/delivery-logs` | List delivery logs | | **GET** | `/api/v1/webhooks/delivery-logs/{deliveryId}` | Get delivery details | | **PATCH** | `/api/v1/webhooks/delivery-logs/{deliveryId}/retry` | Retry failed delivery | --- ## List Delivery Logs Retrieve paginated list of webhook delivery attempts. ### Request **GET** `/api/v1/webhooks/delivery-logs` **Query Parameters:** ### Example Requests ```bash curl -X GET "https://api-sandbox.fndev.net/api/v1/webhooks/delivery-logs?perPage=50" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ```bash curl -X GET "https://api-sandbox.fndev.net/api/v1/webhooks/delivery-logs?deliveryStatus=500" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ```bash curl -X GET "https://api-sandbox.fndev.net/api/v1/webhooks/delivery-logs?webhookId=wh_abc123" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ```bash curl -X GET "https://api-sandbox.fndev.net/api/v1/webhooks/delivery-logs?workOrderId=12345" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ### Response ```json { "metadata": { "timestamp": "2026-01-15T12:00:00Z", "query": { "webhookId": "wh_abc123", "page": 1, "perPage": 50 } }, "result": [ { "deliveryId": "del_xyz789", "webhookId": "wh_abc123", "workOrderId": 12345, "eventName": "workorder.status.published", "deliveryStatus": 200, "deliveryAttempt": 1, "createdAt": "2026-01-15T11:59:00Z" } ] } ``` --- ## Get Delivery Details Retrieve detailed information including pre-signed URL to complete log file. ### Request **GET** `/api/v1/webhooks/delivery-logs/{deliveryId}` ### Example ```bash curl -X GET https://api-sandbox.fndev.net/api/v1/webhooks/delivery-logs/del_xyz789 \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ### Response ```json { "metadata": { "timestamp": "2026-01-15T12:00:00Z" }, "result": { "deliveryId": "del_xyz789", "webhookId": "wh_abc123", "eventName": "workorder.status.published", "deliveryStatus": 200, "deliveryAttempt": 1, "createdAt": "2026-01-15T11:59:00Z", "delivery_log": "https://s3.amazonaws.com/fn-logs/del_xyz789?AWSAccessKeyId=...&Expires=1642252800&Signature=..." } } ``` ![Delivery Logs Dashboard](../images/delivery-logs.webp) --- ## Retry Failed Delivery Manually retry a failed delivery attempt. ### Request **PATCH** `/api/v1/webhooks/delivery-logs/{deliveryId}/retry` ### Example ```bash curl -X PATCH https://api-sandbox.fndev.net/api/v1/webhooks/delivery-logs/del_xyz789/retry \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ### Response ```json { "metadata": { "timestamp": "2026-01-15T12:00:00Z" }, "result": { "job_id": "job_abc123" } } ``` ![Retry Delivery](../images/delivery-log-retry.webp) > [INFO] The `job_id` can be used to track the retry attempt in delivery logs. --- ## Code Examples ```javascript class DeliveryLogMonitor { constructor(accessToken) { this.accessToken = accessToken; this.baseUrl = 'https://api-sandbox.fndev.net'; } async getLogs(filters = {}) { const params = new URLSearchParams(filters); const response = await fetch( `${this.baseUrl}/api/v1/webhooks/delivery-logs?${params}`, { headers: { 'Authorization': `Bearer ${this.accessToken}` } } ); return await response.json(); } async getDeliveryDetails(deliveryId) { const response = await fetch( `${this.baseUrl}/api/v1/webhooks/delivery-logs/${deliveryId}`, { headers: { 'Authorization': `Bearer ${this.accessToken}` } } ); return await response.json(); } async retryDelivery(deliveryId) { const response = await fetch( `${this.baseUrl}/api/v1/webhooks/delivery-logs/${deliveryId}/retry`, { method: 'PATCH', headers: { 'Authorization': `Bearer ${this.accessToken}` } } ); return await response.json(); } async getFailedDeliveries(webhookId, hours = 24) { const since = new Date(Date.now() - hours * 60 * 60 * 1000).toISOString(); const logs = await this.getLogs({ webhookId, sortBy: 'createdAt', sortDirection: 'DESC' }); return logs.result.filter(log => log.deliveryStatus >= 400 && new Date(log.createdAt) >= new Date(since) ); } async retryAllFailed(webhookId) { const failed = await this.getFailedDeliveries(webhookId); const results = []; for (const log of failed) { try { const result = await this.retryDelivery(log.deliveryId); results.push({ deliveryId: log.deliveryId, success: true, jobId: result.result.job_id }); } catch (error) { results.push({ deliveryId: log.deliveryId, success: false, error: error.message }); } } return results; } } // Usage const monitor = new DeliveryLogMonitor(accessToken); // Get failed deliveries const failed = await monitor.getFailedDeliveries('wh_abc123', 24); console.log(`${failed.length} failed deliveries in last 24 hours`); // Retry all failed const results = await monitor.retryAllFailed('wh_abc123'); console.log(`Retried ${results.length} deliveries`); ``` --- --- ### Events URL: /docs/webhooks/api-reference/events ## List Webhook Events Retrieve all available webhook events with their details. ### Request **GET** `/api/v1/webhooks/events` **Headers:** - `Authorization: Bearer {access_token}` (required) **Query Parameters:** ### Example Requests ```bash curl -X GET https://api-sandbox.fndev.net/api/v1/webhooks/events \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ```bash curl -X GET "https://api-sandbox.fndev.net/api/v1/webhooks/events?model=WorkOrder" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ### Response **Status**: `200 OK` ```json { "metadata": { "timestamp": "2026-01-15T12:00:00Z", "query": { "model": null } }, "result": [ { "name": "workorder.created", "label": "Work Order created", "description": "Triggered when a work order is created.", "model": "WorkOrder" }, { "name": "workorder.routed", "label": "Work Order routed", "description": "Triggered when a work order is routed to a provider.", "model": "WorkOrder" }, { "name": "workorder.status.published", "label": "Work Order status published", "description": "Triggered when a work order status changes to published.", "model": "WorkOrder" }, { "name": "workorder.status.assigned", "label": "Work Order status assigned", "description": "Triggered when a work order status changes to assigned.", "model": "WorkOrder" }, { "name": "workorder.status.work_done", "label": "Work Order status work done", "description": "Triggered when a work order status changes to work done.", "model": "WorkOrder" } // ... all 33 events ] } ``` --- ## Event Object Structure Each event in the response includes: --- ## Complete Event List ### Lifecycle Events (13 events) ```json [ { "name": "workorder.created", "label": "Work Order created", "description": "Triggered when a work order is created.", "model": "WorkOrder" }, { "name": "workorder.routed", "label": "Work Order routed", "description": "Triggered when a work order is routed to a provider.", "model": "WorkOrder" }, { "name": "workorder.requested", "label": "Work Order requested", "description": "Triggered when a work order is requested.", "model": "WorkOrder" }, { "name": "workorder.declined", "label": "Work Order declined", "description": "Triggered when a work order is declined by a provider.", "model": "WorkOrder" }, { "name": "workorder.undeclined", "label": "Work Order undeclined", "description": "Triggered when a work order is undeclined by a provider.", "model": "WorkOrder" }, { "name": "workorder.task_completed", "label": "Work Order task completed", "description": "Triggered when a task on a work order is completed.", "model": "WorkOrder" }, { "name": "workorder.task_incomplete", "label": "Work Order task incomplete", "description": "Triggered when a task on a work order is marked as incomplete.", "model": "WorkOrder" }, { "name": "workorder.provider_upload", "label": "Work Order provider upload", "description": "Triggered when a provider uploads a document to a work order.", "model": "WorkOrder" }, { "name": "workorder.message_posted", "label": "Work Order message posted", "description": "Triggered when a message is posted or replied to on a work order.", "model": "WorkOrder" }, { "name": "workorder.custom_field_value_updated", "label": "Work Order custom field value updated", "description": "Triggered when a custom field value is updated on a work order.", "model": "WorkOrder" }, { "name": "workorder.schedule_updated", "label": "Work Order schedule updated", "description": "Triggered when a schedule is updated on a work order.", "model": "WorkOrder" }, { "name": "workorder.tag_added", "label": "Work Order tag added", "description": "Triggered when a tag is added to a work order.", "model": "WorkOrder" }, { "name": "workorder.tag_removed", "label": "Work Order tag removed", "description": "Triggered when a tag is removed from a work order.", "model": "WorkOrder" } ] ``` ### Status Change Events (19 events) ```json [ { "name": "workorder.status.draft", "label": "Work Order status draft", "description": "Triggered when a work order status changes to draft.", "model": "WorkOrder" }, { "name": "workorder.status.routed", "label": "Work Order status routed", "description": "Triggered when a work order status changes to routed.", "model": "WorkOrder" }, { "name": "workorder.status.published", "label": "Work Order status published", "description": "Triggered when a work order status changes to published.", "model": "WorkOrder" }, { "name": "workorder.status.confirmed", "label": "Work Order status confirmed", "description": "Triggered when a work order status changes to confirmed.", "model": "WorkOrder" }, { "name": "workorder.status.assigned", "label": "Work Order status assigned", "description": "Triggered when a work order status changes to assigned.", "model": "WorkOrder" }, { "name": "workorder.status.assigned_cancelled", "label": "Work Order status assigned cancelled", "description": "Triggered when a work order status changes to assigned cancelled.", "model": "WorkOrder" }, { "name": "workorder.status.at_risk", "label": "Work Order status at risk", "description": "Triggered when a work order status changes to at risk.", "model": "WorkOrder" }, { "name": "workorder.status.delayed", "label": "Work Order status delayed", "description": "Triggered when a work order status changes to delayed.", "model": "WorkOrder" }, { "name": "workorder.status.on_my_way", "label": "Work Order status on my way", "description": "Triggered when a work order status changes to on my way.", "model": "WorkOrder" }, { "name": "workorder.status.checked_in", "label": "Work Order status checked in", "description": "Triggered when a work order status changes to checked in.", "model": "WorkOrder" }, { "name": "workorder.status.checked_out", "label": "Work Order status checked out", "description": "Triggered when a work order status changes to checked out.", "model": "WorkOrder" }, { "name": "workorder.status.work_done", "label": "Work Order status work done", "description": "Triggered when a work order status changes to work done.", "model": "WorkOrder" }, { "name": "workorder.status.approved", "label": "Work Order status approved", "description": "Triggered when a work order status changes to approved.", "model": "WorkOrder" }, { "name": "workorder.status.paid", "label": "Work Order status paid", "description": "Triggered when a work order status changes to paid.", "model": "WorkOrder" }, { "name": "workorder.status.cancelled", "label": "Work Order status cancelled", "description": "Triggered when a work order status changes to cancelled.", "model": "WorkOrder" }, { "name": "workorder.status.deleted", "label": "Work Order status deleted", "description": "Triggered when a work order status changes to deleted.", "model": "WorkOrder" }, { "name": "workorder.status.postponed", "label": "Work Order status postponed", "description": "Triggered when a work order status changes to postponed.", "model": "WorkOrder" }, { "name": "workorder.problem_reported", "label": "Work Order problem reported", "description": "Triggered when a problem is reported or reopened on a work order.", "model": "WorkOrder" }, { "name": "workorder.problem_resolved", "label": "Work Order problem resolved", "description": "Triggered when a problem is resolved on a work order.", "model": "WorkOrder" } ] ``` ### Additional Event ```json { "name": "workorder.part_updated", "label": "Work Order part updated", "description": "Triggered when parts/materials on a work order are updated.", "model": "WorkOrder" } ``` --- ## Usage Examples ### Dynamic Event Selection UI Build a dynamic event selector using the API: ```javascript async function buildEventSelector() { // Fetch available events const response = await fetch( 'https://api-sandbox.fndev.net/api/v1/webhooks/events', { headers: { 'Authorization': `Bearer ${accessToken}` } } ); const { result: events } = await response.json(); // Group by category const grouped = { lifecycle: events.filter(e => !e.name.includes('status.')), status: events.filter(e => e.name.includes('status.')) }; // Render UI return { lifecycleEvents: grouped.lifecycle.map(e => ({ value: e.name, label: e.label, description: e.description })), statusEvents: grouped.status.map(e => ({ value: e.name, label: e.label, description: e.description })) }; } ``` ### Validate Event Names Ensure event names are valid before creating webhooks: ```javascript async function validateEvents(eventNames) { // Get available events const response = await fetch( 'https://api-sandbox.fndev.net/api/v1/webhooks/events', { headers: { 'Authorization': `Bearer ${accessToken}` } } ); const { result: events } = await response.json(); const validEvents = new Set(events.map(e => e.name)); // Check each event const invalid = eventNames.filter(name => !validEvents.has(name)); if (invalid.length > 0) { throw new Error(`Invalid events: ${invalid.join(', ')}`); } return true; } // Usage await validateEvents([ 'workorder.status.published', 'workorder.status.assigned', 'invalid.event.name' // Will throw error ]); ``` ### Cache Events Locally Reduce API calls by caching event metadata: ```javascript class EventCache { constructor(accessToken) { this.accessToken = accessToken; this.cache = null; this.cacheExpiry = null; } async getEvents() { // Return cache if valid (cache for 24 hours) if (this.cache && this.cacheExpiry > Date.now()) { return this.cache; } // Fetch from API const response = await fetch( 'https://api-sandbox.fndev.net/api/v1/webhooks/events', { headers: { 'Authorization': `Bearer ${this.accessToken}` } } ); const { result } = await response.json(); // Cache for 24 hours this.cache = result; this.cacheExpiry = Date.now() + (24 * 60 * 60 * 1000); return result; } async getEventByName(eventName) { const events = await this.getEvents(); return events.find(e => e.name === eventName); } async getEventsByModel(model) { const events = await this.getEvents(); return events.filter(e => e.model === model); } } // Usage const cache = new EventCache(accessToken); // Get all events const allEvents = await cache.getEvents(); // Get specific event const event = await cache.getEventByName('workorder.status.published'); // Get by model const workOrderEvents = await cache.getEventsByModel('WorkOrder'); ``` --- ## Best Practices ### Always Fetch Latest Events Event list may change as Field Nation adds new events: ```javascript // ❌ Don't hardcode events const events = [ 'workorder.status.published', 'workorder.status.assigned' ]; // ✅ Do fetch from API const response = await fetch('/api/v1/webhooks/events'); const { result: events } = await response.json(); ``` ### Validate Before Creating Webhooks ```javascript async function createValidatedWebhook(config) { // 1. Fetch valid events const eventsResponse = await fetch( 'https://api-sandbox.fndev.net/api/v1/webhooks/events', { headers: { 'Authorization': `Bearer ${accessToken}` } } ); const { result: validEvents } = await eventsResponse.json(); const validEventNames = new Set(validEvents.map(e => e.name)); // 2. Validate config events const invalid = config.events.filter(name => !validEventNames.has(name)); if (invalid.length > 0) { throw new Error(`Invalid events: ${invalid.join(', ')}`); } // 3. Create 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(config) } ); return await response.json(); } ``` ### Document Your Subscriptions ```javascript async function documentWebhookEvents(webhookId) { const webhook = await getWebhook(webhookId); const allEvents = await getEvents(); const subscribedEvents = webhook.result.events.map(eventName => { const event = allEvents.find(e => e.name === eventName); return { name: eventName, label: event?.label || 'Unknown', description: event?.description || 'No description' }; }); console.log(`\nWebhook ${webhookId} subscribed to ${subscribedEvents.length} events:\n`); subscribedEvents.forEach(event => { console.log(` • ${event.label}`); console.log(` ${event.description}`); }); } ``` --- --- ### History URL: /docs/webhooks/api-reference/history ## Get Webhook History Retrieve paginated change history for a specific webhook. ### Request **GET** `/api/v1/webhooks/{webhookId}/history` **Query Parameters:** ### Example Requests ```bash curl -X GET https://api-sandbox.fndev.net/api/v1/webhooks/wh_abc123/history \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ```bash curl -X GET "https://api-sandbox.fndev.net/api/v1/webhooks/wh_abc123/history?userId=456" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ```bash curl -X GET "https://api-sandbox.fndev.net/api/v1/webhooks/wh_abc123/history?search=status_changed" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ### Response ```json { "metadata": { "timestamp": "2026-01-15T12:00:00Z", "count": 5, "total": 5 }, "result": [ { "id": 123, "userId": 456, "action": "status_changed", "changes": { "status": { "from": "active", "to": "inactive" } }, "createdAt": "2026-01-15T10:45:00Z", "updatedAt": "2026-01-15T10:45:00Z" }, { "id": 122, "userId": 456, "action": "events_updated", "changes": { "events": { "added": ["workorder.status.approved"], "removed": ["workorder.status.draft"] } }, "createdAt": "2026-01-15T09:30:00Z", "updatedAt": "2026-01-15T09:30:00Z" }, { "id": 121, "userId": 456, "action": "url_changed", "changes": { "url": { "from": "https://old-endpoint.com/webhook", "to": "https://new-endpoint.com/webhooks" } }, "createdAt": "2026-01-14T15:20:00Z", "updatedAt": "2026-01-14T15:20:00Z" }, { "id": 120, "userId": 456, "action": "webhook_created", "changes": { "url": "https://new-endpoint.com/webhooks", "events": ["workorder.status.published", "workorder.status.draft"], "status": "active" }, "createdAt": "2026-01-14T15:00:00Z", "updatedAt": "2026-01-14T15:00:00Z" } ] } ``` ![Webhook Change History](../images/webhook-update-history.webp) --- ## Change Record Structure --- ## Common Action Types | Action | Description | Changes Object | |--------|-------------|----------------| | `webhook_created` | Webhook created | Initial configuration | | `status_changed` | Status modified | `{ status: { from, to } }` | | `url_changed` | URL updated | `{ url: { from, to } }` | | `events_updated` | Events modified | `{ events: { added: [], removed: [] } }` | | `webhook_updated` | General update | Modified fields | | `webhook_deleted` | Webhook deleted | Final configuration | --- ## Code Examples ```javascript class WebhookAudit { constructor(accessToken) { this.accessToken = accessToken; this.baseUrl = 'https://api-sandbox.fndev.net'; } async getHistory(webhookId, filters = {}) { const params = new URLSearchParams(filters); const response = await fetch( `${this.baseUrl}/api/v1/webhooks/${webhookId}/history?${params}`, { headers: { 'Authorization': `Bearer ${this.accessToken}` } } ); return await response.json(); } async getChangesByUser(webhookId, userId) { return await this.getHistory(webhookId, { userId }); } async getRecentChanges(webhookId, hours = 24) { const history = await this.getHistory(webhookId, { sortBy: 'createdAt', sortDirection: 'DESC' }); const since = new Date(Date.now() - hours * 60 * 60 * 1000); return history.result.filter(record => new Date(record.createdAt) >= since ); } async generateAuditReport(webhookId) { const history = await this.getHistory(webhookId); const report = { webhookId, totalChanges: history.metadata.total, changes: history.result.map(record => ({ timestamp: record.createdAt, user: record.userId, action: record.action, details: this.formatChanges(record.changes) })) }; return report; } formatChanges(changes) { return Object.entries(changes).map(([key, value]) => { if (value.from && value.to) { return `${key}: ${value.from} → ${value.to}`; } if (value.added && value.removed) { return `${key}: +${value.added.length} -${value.removed.length}`; } return `${key}: ${JSON.stringify(value)}`; }).join(', '); } } // Usage const audit = new WebhookAudit(accessToken); // Get full history const history = await audit.getHistory('wh_abc123'); // Get changes by specific user const userChanges = await audit.getChangesByUser('wh_abc123', 456); // Get recent changes const recent = await audit.getRecentChanges('wh_abc123', 24); // Generate audit report const report = await audit.generateAuditReport('wh_abc123'); console.log(`Webhook has ${report.totalChanges} changes`); report.changes.forEach(change => { console.log(` ${change.timestamp} - ${change.action} by user ${change.user}`); }); ``` --- --- ### Overview URL: /docs/webhooks/api-reference/overview ## Base Information ### API Version **Current Version**: v3.0 **Base URL (Sandbox)**: `https://api-sandbox.fndev.net` **Base URL (Production)**: Contact Field Nation support for production access ### OpenAPI Specification - **Swagger UI (Sandbox)**: [https://ui-sandbox.fndev.net/integrations/webhooks/_api](https://ui-sandbox.fndev.net/integrations/webhooks/_api) - **JSON Spec (Sandbox)**: [https://ui-sandbox.fndev.net/integrations/webhooks/_api-json](https://ui-sandbox.fndev.net/integrations/webhooks/_api-json) --- ## Authentication The Webhooks API uses OAuth 2.0 Bearer tokens for authentication. ### Getting an Access Token ### Obtain Credentials Request API credentials from Field Nation: - `client_id` - `client_secret` ### Generate Access Token ```bash curl -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:** ```json { "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", "token_type": "Bearer", "expires_in": 3600 } ``` ### Use Token in Requests ```bash curl -X GET https://api-sandbox.fndev.net/api/v1/webhooks \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` > **Token Expiration**: Access tokens expire after 1 hour. Implement token refresh logic for production applications. ### Token Refresh ```javascript class FieldNationAuth { constructor(clientId, clientSecret) { this.clientId = clientId; this.clientSecret = clientSecret; this.accessToken = null; this.tokenExpiry = null; } async getAccessToken() { // Return cached token if still valid if (this.accessToken && this.tokenExpiry > Date.now()) { return this.accessToken; } // Request new token const response = await fetch( 'https://api-sandbox.fndev.net/authentication/api/oauth/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'client_credentials', client_id: this.clientId, client_secret: this.clientSecret }) } ); const data = await response.json(); this.accessToken = data.access_token; // Refresh 5 minutes before expiry this.tokenExpiry = Date.now() + (data.expires_in - 300) * 1000; return this.accessToken; } } // Usage const auth = new FieldNationAuth(CLIENT_ID, CLIENT_SECRET); const token = await auth.getAccessToken(); ``` --- ## API Endpoints The Webhooks API is organized into 5 main categories: ### Core Operations Manage webhook lifecycle (CRUD operations): | Method | Endpoint | Description | |--------|----------|-------------| | **GET** | `/api/v1/webhooks` | List all webhooks | | **POST** | `/api/v1/webhooks` | Create a webhook | | **GET** | `/api/v1/webhooks/{webhookId}` | Get webhook details | | **PUT** | `/api/v1/webhooks/{webhookId}` | Update a webhook | | **DELETE** | `/api/v1/webhooks/{webhookId}` | Delete a webhook | [Complete reference →](/docs/webhooks/api-reference/webhooks) ### Events Discover available webhook events: | Method | Endpoint | Description | |--------|----------|-------------| | **GET** | `/api/v1/webhooks/events` | List available events | [Complete reference →](/docs/webhooks/api-reference/events) ### Delivery Logs Monitor webhook deliveries: | Method | Endpoint | Description | |--------|----------|-------------| | **GET** | `/api/v1/webhooks/delivery-logs` | List delivery logs | | **GET** | `/api/v1/webhooks/delivery-logs/{deliveryId}` | Get delivery details | | **PATCH** | `/api/v1/webhooks/delivery-logs/{deliveryId}/retry` | Retry failed delivery | [Complete reference →](/docs/webhooks/api-reference/delivery-logs) ### History Audit webhook changes: | Method | Endpoint | Description | |--------|----------|-------------| | **GET** | `/api/v1/webhooks/{webhookId}/history` | Get webhook change history | [Complete reference →](/docs/webhooks/api-reference/history) ### Attributes Manage custom headers and legacy fields: | Method | Endpoint | Description | |--------|----------|-------------| | **DELETE** | `/api/v1/webhooks/{webhookId}/{attributeType}/{attributeName}` | Delete webhook attribute | --- ## Common Request Patterns ### Pagination List endpoints support pagination: ```bash curl -X GET "https://api-sandbox.fndev.net/api/v1/webhooks?page=1&perPage=25" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` **Parameters:** - `page` (integer): Page number, starts at 1. Default: 1 - `perPage` (integer): Items per page. Default: 25 **Response includes pagination metadata:** ```json { "metadata": { "timestamp": "2026-01-15T12:00:00Z", "count": 25, "total": 150 }, "result": [...] } ``` ### Filtering Filter results by specific fields: ```bash # Filter by status curl -X GET "https://api-sandbox.fndev.net/api/v1/webhooks?status=active" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" # Filter by webhook ID curl -X GET "https://api-sandbox.fndev.net/api/v1/webhooks/delivery-logs?webhookId=wh_abc123" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ### Sorting Sort results by specific fields: ```bash curl -X GET "https://api-sandbox.fndev.net/api/v1/webhooks?sortBy=createdAt&sortDirection=DESC" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` **Parameters:** - `sortBy` (string): Field to sort by (e.g., `id`, `createdAt`, `updatedAt`) - `sortDirection` (string): `ASC` or `DESC` ### Field Selection Request only specific fields: ```bash curl -X GET "https://api-sandbox.fndev.net/api/v1/webhooks/{webhookId}?fields=id,webhookId,url,status,events" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ### Searching Search across searchable fields: ```bash curl -X GET "https://api-sandbox.fndev.net/api/v1/webhooks?search=production" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` --- ## Response Format ### Success Response ```json { "metadata": { "timestamp": "2026-01-15T12:00:00Z", "count": 1, "total": 1 }, "result": { // Response data } } ``` ### Error Response ```json { "metadata": { "timestamp": "2026-01-15T12:00:00Z", "path": "/api/v1/webhooks" }, "errors": [ { "code": 400, "message": "Invalid request: missing required field 'url'" } ], "result": {} } ``` ### HTTP Status Codes | Status | Meaning | |--------|---------| | **200** | Success | | **201** | Created | | **400** | Bad Request - Invalid parameters | | **401** | Unauthorized - Invalid or missing token | | **403** | Forbidden - Insufficient permissions | | **404** | Not Found - Resource doesn't exist | | **422** | Unprocessable Entity - Validation failed | | **429** | Too Many Requests - Rate limit exceeded | | **500** | Internal Server Error | | **503** | Service Unavailable | --- ## Rate Limiting The API enforces rate limits to ensure fair usage: - **Rate Limit**: 100 requests per minute per client - **Burst**: 20 requests **Rate limit headers:** ```http X-RateLimit-Limit: 100 X-RateLimit-Remaining: 85 X-RateLimit-Reset: 1642252800 ``` ### Handling Rate Limits ```javascript async function makeAPIRequest(url, options) { const response = await fetch(url, options); if (response.status === 429) { const resetTime = parseInt(response.headers.get('X-RateLimit-Reset')); const waitTime = (resetTime * 1000) - Date.now(); console.log(`Rate limit exceeded. Waiting ${waitTime}ms...`); await sleep(waitTime); // Retry return await makeAPIRequest(url, options); } return response; } ``` --- ## Best Practices ### Error Handling Always handle API errors gracefully: ```javascript async function createWebhook(config) { try { const response = await fetch( 'https://api-sandbox.fndev.net/api/v1/webhooks', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify(config) } ); if (!response.ok) { const error = await response.json(); throw new Error(`API Error: ${error.errors[0].message}`); } return await response.json(); } catch (error) { console.error('Failed to create webhook:', error); throw error; } } ``` ### Retry Logic Implement exponential backoff for transient errors: ```javascript async function apiRequestWithRetry(url, options, maxRetries = 3) { for (let attempt = 1; attempt <= maxRetries; attempt++) { try { const response = await fetch(url, options); // Retry on 5xx errors if (response.status >= 500) { if (attempt === maxRetries) { throw new Error(`Max retries exceeded: ${response.status}`); } const delay = Math.pow(2, attempt) * 1000; // Exponential backoff console.log(`Attempt ${attempt} failed, retrying in ${delay}ms...`); await sleep(delay); continue; } return response; } catch (error) { if (attempt === maxRetries) throw error; const delay = Math.pow(2, attempt) * 1000; console.log(`Attempt ${attempt} failed, retrying in ${delay}ms...`); await sleep(delay); } } } ``` ### Caching Tokens Cache access tokens to reduce authentication requests: ```javascript const tokenCache = { token: null, expiry: 0 }; async function getToken() { const now = Date.now(); // Return cached token if valid if (tokenCache.token && tokenCache.expiry > now) { return tokenCache.token; } // Fetch new token const response = await fetch(/* auth request */); const data = await response.json(); tokenCache.token = data.access_token; tokenCache.expiry = now + (data.expires_in - 300) * 1000; return tokenCache.token; } ``` ### Logging Requests Log API requests for debugging: ```javascript async function loggedAPIRequest(url, options) { const requestId = generateRequestId(); console.log(`[${requestId}] Request: ${options.method} ${url}`); console.log(`[${requestId}] Headers:`, options.headers); const startTime = Date.now(); const response = await fetch(url, options); const duration = Date.now() - startTime; console.log(`[${requestId}] Response: ${response.status} (${duration}ms)`); return response; } ``` --- ## SDK Example Wrapper class for cleaner API interactions: ```javascript title="fieldnation-webhooks-sdk.js" class FieldNationWebhooksAPI { constructor(clientId, clientSecret, baseUrl) { this.clientId = clientId; this.clientSecret = clientSecret; this.baseUrl = baseUrl || 'https://api-sandbox.fndev.net'; this.accessToken = null; this.tokenExpiry = null; } async getAccessToken() { if (this.accessToken && this.tokenExpiry > Date.now()) { return this.accessToken; } const response = await fetch( `${this.baseUrl}/authentication/api/oauth/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'client_credentials', client_id: this.clientId, client_secret: this.clientSecret }) } ); const data = await response.json(); this.accessToken = data.access_token; this.tokenExpiry = Date.now() + (data.expires_in - 300) * 1000; return this.accessToken; } async request(method, path, body = null) { const token = await this.getAccessToken(); const options = { method, headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } }; if (body) { options.body = JSON.stringify(body); } const response = await fetch(`${this.baseUrl}${path}`, options); if (!response.ok) { const error = await response.json(); throw new Error(`API Error: ${JSON.stringify(error.errors)}`); } return await response.json(); } // Webhook Operations async listWebhooks(params = {}) { const query = new URLSearchParams(params); return await this.request('GET', `/api/v1/webhooks?${query}`); } async getWebhook(webhookId) { return await this.request('GET', `/api/v1/webhooks/${webhookId}`); } async createWebhook(config) { return await this.request('POST', '/api/v1/webhooks', config); } async updateWebhook(webhookId, updates) { return await this.request('PUT', `/api/v1/webhooks/${webhookId}`, updates); } async deleteWebhook(webhookId) { return await this.request('DELETE', `/api/v1/webhooks/${webhookId}`); } // Delivery Logs async getDeliveryLogs(params = {}) { const query = new URLSearchParams(params); return await this.request('GET', `/api/v1/webhooks/delivery-logs?${query}`); } async retryDelivery(deliveryId) { return await this.request('PATCH', `/api/v1/webhooks/delivery-logs/${deliveryId}/retry`); } // Events async getAvailableEvents(model = null) { const query = model ? `?model=${model}` : ''; return await this.request('GET', `/api/v1/webhooks/events${query}`); } } // Usage const api = new FieldNationWebhooksAPI(CLIENT_ID, CLIENT_SECRET); // List webhooks const webhooks = await api.listWebhooks({ status: 'active' }); // Create webhook const webhook = await api.createWebhook({ url: 'https://example.com/webhooks', method: 'post', status: 'active', events: ['workorder.status.published'] }); // Get delivery logs const logs = await api.getDeliveryLogs({ webhookId: webhook.result.webhookId }); ``` --- --- ### Webhooks URL: /docs/webhooks/api-reference/webhooks ## Endpoints Overview | Method | Endpoint | Description | |--------|----------|-------------| | **POST** | `/api/v1/webhooks` | Create a new webhook | | **GET** | `/api/v1/webhooks` | List all webhooks | | **GET** | `/api/v1/webhooks/{webhookId}` | Get webhook details | | **PUT** | `/api/v1/webhooks/{webhookId}` | Update a webhook | | **DELETE** | `/api/v1/webhooks/{webhookId}` | Delete a webhook | --- ## Create Webhook Create a new webhook configuration. ### Request **POST** `/api/v1/webhooks` **Headers:** - `Authorization: Bearer {access_token}` (required) - `Content-Type: application/json` (required) **Body Parameters:** ", description: "Array of event names to subscribe to. Must contain at least 1 event.", required: true }, "secret": { type: "string (UUID)", description: "Optional secret for HMAC-SHA256 signing. Auto-generated if omitted.", required: false }, "notificationEmail": { type: "string (email)", description: "Email address for delivery failure notifications.", required: false }, "isIntegrationOnly": { type: "boolean", description: "Whether webhook is restricted to API-only access.", required: false, default: false }, "webhookAttribute": { type: "object", description: "Custom headers and legacy field mappings.", required: false } }} /> ### Example Request ```bash 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.status.published", "workorder.status.assigned", "workorder.status.work_done", "workorder.status.approved" ], "notificationEmail": "webhook-alerts@example.com", "webhookAttribute": { "header": { "Authorization": "Bearer your-api-token", "X-Custom-ID": "prod-webhook-001" } } }' ``` ### Response **Status**: `200 OK` ```json { "metadata": { "timestamp": "2026-01-15T10:30:00Z" }, "result": { "id": 123, "webhookId": "wh_abc123def456", "companyId": 789, "userId": 456, "url": "https://your-endpoint.com/webhooks", "method": "post", "status": "active", "secret": "01999f51-5c66-4449-b441-6b4a053fee6a", "events": [ "workorder.status.published", "workorder.status.assigned", "workorder.status.work_done", "workorder.status.approved" ], "notificationEmail": "webhook-alerts@example.com", "modelProperties": [], "isIntegrationOnly": false, "createdAt": "2026-01-15T10:30:00Z", "updatedAt": "2026-01-15T10:30:00Z" } } ``` > **Save the secret!** The `secret` field is only returned during creation. Store it securely for signature verification. --- ## List Webhooks Retrieve a paginated list of webhooks. ### Request **GET** `/api/v1/webhooks` **Headers:** - `Authorization: Bearer {access_token}` (required) **Query Parameters:** ### Example Requests ```bash curl -X GET "https://api-sandbox.fndev.net/api/v1/webhooks" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ```bash curl -X GET "https://api-sandbox.fndev.net/api/v1/webhooks?status=active" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ```bash curl -X GET "https://api-sandbox.fndev.net/api/v1/webhooks?search=production&sortBy=createdAt&sortDirection=DESC" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ```bash curl -X GET "https://api-sandbox.fndev.net/api/v1/webhooks?fields=id,webhookId,url,status,events" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ### Response **Status**: `200 OK` ```json { "metadata": { "timestamp": "2026-01-15T11:00:00Z", "count": 2, "total": 2 }, "result": [ { "id": 123, "webhookId": "wh_abc123", "companyId": 789, "userId": 456, "url": "https://your-endpoint.com/webhooks", "method": "post", "status": "active", "secret": "01999f51-5c66-4449-b441-6b4a053fee6a", "events": ["workorder.status.published", "workorder.status.assigned"], "notificationEmail": "alerts@example.com", "modelProperties": [], "isIntegrationOnly": false, "createdAt": "2026-01-15T10:00:00Z", "updatedAt": "2026-01-15T10:00:00Z" }, { "id": 124, "webhookId": "wh_def456", "companyId": 789, "userId": 456, "url": "https://staging.example.com/webhooks", "method": "post", "status": "inactive", "secret": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "events": ["workorder.status.work_done"], "notificationEmail": "staging@example.com", "modelProperties": [], "isIntegrationOnly": false, "createdAt": "2026-01-14T15:00:00Z", "updatedAt": "2026-01-15T09:00:00Z" } ] } ``` --- ## Get Webhook Details Retrieve detailed information about a specific webhook. ### Request **GET** `/api/v1/webhooks/{webhookId}` **Headers:** - `Authorization: Bearer {access_token}` (required) **Path Parameters:** - `webhookId` (string, required): Unique webhook identifier **Query Parameters:** ### Example Requests ```bash curl -X GET https://api-sandbox.fndev.net/api/v1/webhooks/wh_abc123 \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ```bash curl -X GET "https://api-sandbox.fndev.net/api/v1/webhooks/wh_abc123?webhookAttribute=header" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ### Response **Status**: `200 OK` ```json { "metadata": { "timestamp": "2026-01-15T11:00:00Z" }, "result": { "id": 123, "webhookId": "wh_abc123", "companyId": 789, "userId": 456, "url": "https://your-endpoint.com/webhooks", "method": "post", "status": "active", "secret": "01999f51-5c66-4449-b441-6b4a053fee6a", "events": [ "workorder.status.published", "workorder.status.assigned" ], "notificationEmail": "alerts@example.com", "modelProperties": [], "isIntegrationOnly": false, "createdAt": "2026-01-15T10:00:00Z", "updatedAt": "2026-01-15T10:30:00Z" } } ``` --- ## Update Webhook Update an existing webhook configuration. Only include fields you want to change. ### Request **PUT** `/api/v1/webhooks/{webhookId}` **Headers:** - `Authorization: Bearer {access_token}` (required) - `Content-Type: application/json` (required) **Path Parameters:** - `webhookId` (string, required): Unique webhook identifier **Body Parameters** (all optional): ", description: "Updated array of event names (replaces existing list)", required: false }, "secret": { type: "string (UUID)", description: "Updated secret for signature verification", required: false }, "notificationEmail": { type: "string (email)", description: "Updated notification email", required: false }, "webhookAttribute": { type: "object", description: "Updated custom headers", required: false } }} /> ### Example Requests ```bash curl -X PUT https://api-sandbox.fndev.net/api/v1/webhooks/wh_abc123 \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "url": "https://new-endpoint.com/webhooks" }' ``` ```bash curl -X PUT https://api-sandbox.fndev.net/api/v1/webhooks/wh_abc123 \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "status": "inactive" }' ``` ```bash curl -X PUT https://api-sandbox.fndev.net/api/v1/webhooks/wh_abc123 \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "events": [ "workorder.status.published", "workorder.status.assigned", "workorder.status.work_done", "workorder.status.approved" ] }' ``` ```bash curl -X PUT https://api-sandbox.fndev.net/api/v1/webhooks/wh_abc123 \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "webhookAttribute": { "header": { "X-Environment": "production", "X-Version": "v2" } } }' ``` ### Response **Status**: `200 OK` ```json { "metadata": { "timestamp": "2026-01-15T11:30:00Z" }, "result": { "id": 123, "webhookId": "wh_abc123", "companyId": 789, "userId": 456, "url": "https://new-endpoint.com/webhooks", "method": "post", "status": "active", "secret": "01999f51-5c66-4449-b441-6b4a053fee6a", "events": [ "workorder.status.published", "workorder.status.assigned" ], "notificationEmail": "alerts@example.com", "modelProperties": [], "isIntegrationOnly": false, "createdAt": "2026-01-15T10:00:00Z", "updatedAt": "2026-01-15T11:30:00Z" } } ``` --- ## Delete Webhook Permanently delete a webhook configuration. Delivery logs are preserved. ### Request **DELETE** `/api/v1/webhooks/{webhookId}` **Headers:** - `Authorization: Bearer {access_token}` (required) **Path Parameters:** - `webhookId` (string, required): Unique webhook identifier ### Example Request ```bash curl -X DELETE https://api-sandbox.fndev.net/api/v1/webhooks/wh_abc123 \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ### Response **Status**: `200 OK` ```json { "metadata": { "timestamp": "2026-01-15T12:00:00Z" }, "result": { "id": 123, "webhookId": "wh_abc123", "companyId": 789, "userId": 456, "url": "https://your-endpoint.com/webhooks", "method": "post", "status": "active", "secret": "01999f51-5c66-4449-b441-6b4a053fee6a", "events": ["workorder.status.published"], "notificationEmail": "alerts@example.com", "modelProperties": [], "isIntegrationOnly": false, "createdAt": "2026-01-15T10:00:00Z", "updatedAt": "2026-01-15T10:00:00Z" } } ``` > **Irreversible**: Deleted webhooks cannot be recovered. Consider archiving (`status: "archived"`) instead if you want to preserve the configuration. --- ## Delete Webhook Attribute Remove a specific custom header or legacy field mapping. ### Request **DELETE** `/api/v1/webhooks/{webhookId}/{attributeType}/{attributeName}` **Headers:** - `Authorization: Bearer {access_token}` (required) **Path Parameters:** - `webhookId` (string, required): Unique webhook identifier - `attributeType` (string, required): Type of attribute: `header`, `legacy_field` - `attributeName` (string, required): Name of the attribute to delete ### Example Request ```bash # Delete custom header curl -X DELETE https://api-sandbox.fndev.net/api/v1/webhooks/wh_abc123/header/X-Custom-ID \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ### Response **Status**: `200 OK` ```json { "message": "Webhook attribute successfully removed" } ``` --- ## Error Responses ### 400 Bad Request ```json { "metadata": { "timestamp": "2026-01-15T12:00:00Z", "path": "/api/v1/webhooks" }, "errors": [ { "code": 400, "message": "Invalid request: 'url' must be a valid HTTPS URL" } ], "result": {} } ``` ### 401 Unauthorized ```json { "metadata": { "timestamp": "2026-01-15T12:00:00Z", "path": "/api/v1/webhooks" }, "errors": [ { "code": 401, "message": "Invalid or expired access token" } ], "result": {} } ``` ### 404 Not Found ```json { "metadata": { "timestamp": "2026-01-15T12:00:00Z", "path": "/api/v1/webhooks/wh_invalid" }, "errors": [ { "code": 404, "message": "Webhook not found" } ], "result": {} } ``` --- ## Code Examples ### Complete CRUD Operations ```javascript class WebhookManager { constructor(accessToken) { this.accessToken = accessToken; this.baseUrl = 'https://api-sandbox.fndev.net'; } async create(config) { const response = await fetch(`${this.baseUrl}/api/v1/webhooks`, { method: 'POST', headers: { 'Authorization': `Bearer ${this.accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify(config) }); return await response.json(); } async list(filters = {}) { const params = new URLSearchParams(filters); const response = await fetch( `${this.baseUrl}/api/v1/webhooks?${params}`, { headers: { 'Authorization': `Bearer ${this.accessToken}` } } ); return await response.json(); } async get(webhookId) { const response = await fetch( `${this.baseUrl}/api/v1/webhooks/${webhookId}`, { headers: { 'Authorization': `Bearer ${this.accessToken}` } } ); return await response.json(); } async update(webhookId, updates) { const response = await fetch( `${this.baseUrl}/api/v1/webhooks/${webhookId}`, { method: 'PUT', headers: { 'Authorization': `Bearer ${this.accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify(updates) } ); return await response.json(); } async delete(webhookId) { const response = await fetch( `${this.baseUrl}/api/v1/webhooks/${webhookId}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${this.accessToken}` } } ); return await response.json(); } } // Usage const manager = new WebhookManager(accessToken); // Create const webhook = await manager.create({ url: 'https://example.com/webhooks', method: 'post', status: 'active', events: ['workorder.status.published'] }); // List const webhooks = await manager.list({ status: 'active' }); // Get const details = await manager.get('wh_abc123'); // Update const updated = await manager.update('wh_abc123', { status: 'inactive' }); // Delete const deleted = await manager.delete('wh_abc123'); ``` --- --- ### Delivery URL: /docs/webhooks/concepts/delivery ## Delivery Flow ```mermaid sequenceDiagram participant Event as Event Occurs participant Queue as Message Queue participant Delivery as Delivery Service participant Your as Your Endpoint participant DLQ as Dead Letter Queue Event->>Queue: Enqueue webhook job Queue->>Delivery: Pop job for delivery Delivery->>Your: POST webhook payload alt 2xx Response Your-->>Delivery: 200 OK Delivery->>Queue: Mark success, log delivery else Non-2xx or Timeout Your-->>Delivery: 500 Error Delivery->>Queue: Requeue with delay Note over Queue: Exponential backoff end alt After Max Retries Queue->>DLQ: Move to dead letter queue DLQ->>Delivery: Available for manual retry end ``` --- ## Delivery Lifecycle ### Event Triggered When an event occurs in Field Nation (e.g., work order published), the system identifies all webhooks subscribed to that event. ### Job Creation For each subscribed webhook, a delivery job is created and added to the message queue with: - Webhook configuration - Event payload - Delivery metadata - Retry counter (starts at 0) ### Immediate Delivery Attempt The delivery service picks up the job and sends an HTTP POST/PUT request to your endpoint: ```http POST /webhooks/fieldnation HTTP/1.1 Host: your-endpoint.com Content-Type: application/json x-fn-signature: sha256=abc123... x-fn-webhook-id: wh_abc123 x-fn-event-name: workorder.created x-fn-delivery-id: del_xyz789 x-fn-timestamp: 2026-01-15T10:30:00Z {webhook payload} ``` ### Response Evaluation Your endpoint's response determines the next step: | Response | Action | |----------|--------| | **2xx (Success)** | Mark delivery successful, log, done | | **404, 410** | Hard failure, skip retries, log error | | **Other 4xx** | Client error, skip retries, log error | | **5xx** | Server error, schedule retry with backoff | | **Timeout (30s)** | Network issue, schedule retry | | **Connection Error** | Network issue, schedule retry | ### Retry or Complete - **Success (2xx)**: Job complete, delivery logged - **Hard failure (404/410)**: Job failed permanently, no retry - **Retriable error**: Job requeued with exponential delay --- ## Retry Strategy Field Nation uses **exponential backoff** to retry failed deliveries, giving your system time to recover from temporary issues. ### Retry Schedule | Attempt | Delay After Previous | Total Time Elapsed | Notes | |---------|---------------------|-------------------|-------| | 1 (Initial) | 0 seconds | 0 sec | Immediate | | 2 | 10 seconds | 10 sec | - | | 3 | 20 seconds | 30 sec | - | | 4 | 40 seconds | 1 min 10 sec | - | | 5 | 80 seconds | 2 min 30 sec | - | | 6 | 160 seconds | 5 min 10 sec | - | | 7 | 320 seconds | 10 min 30 sec | - | | 8 | 640 seconds | 21 min 10 sec | Final attempt | ### Dynamic Retry Count The maximum retry attempts are **dynamic** based on your webhook's success rate: - **High success rate (>95%)**: Up to 7 retries - **Medium success rate (80-95%)**: Up to 5 retries - **Low success rate (<80%)**: Minimum 3 retries This adaptive approach: - ✅ Rewards reliable endpoints with more retry attempts - ⚠️ Reduces load on consistently failing endpoints - 🎯 Optimizes resource usage > [INFO] **Initial Setup**: New webhooks start with 7 retry attempts. Success rate is calculated after ~100 deliveries. ### Backoff Formula Each retry doubles the previous delay: ```javascript function calculateDelay(attemptNumber) { const baseDelay = 10; // seconds return baseDelay * Math.pow(2, attemptNumber - 2); } // Attempt 2: 10 * 2^0 = 10 seconds // Attempt 3: 10 * 2^1 = 20 seconds // Attempt 4: 10 * 2^2 = 40 seconds // ... ``` --- ## Hard Failures (No Retry) Certain errors indicate permanent problems and skip retries entirely: ### 404 Not Found ```http HTTP/1.1 404 Not Found ``` **Meaning**: Your endpoint doesn't exist or path is incorrect **Action**: No retry, delivery marked as permanently failed **Fix**: Update webhook URL configuration --- ### 410 Gone ```http HTTP/1.1 410 Gone ``` **Meaning**: Endpoint permanently removed **Action**: No retry, delivery marked as permanently failed **Fix**: Delete webhook or update URL --- ## Message Queue Architecture Field Nation uses Redis-based queuing with multiple priority levels: ### Queue Priority 1. **High Priority**: Critical events (payment, approval) 2. **Normal Priority**: Standard events (status changes) 3. **Low Priority**: Non-critical events (messages, uploads) ### Queue Management ```plaintext ┌─────────────────┐ │ Event Occurs │ └────────┬────────┘ │ ▼ ┌─────────────────┐ │ Create Job │ │ - Payload │ │ - Retry: 0 │ │ - Priority │ └────────┬────────┘ │ ▼ ┌─────────────────┐ │ Message Queue │ │ (Redis) │ └────────┬────────┘ │ ▼ ┌─────────────────┐ │ Delivery Worker │ │ Pick & Deliver │ └────────┬────────┘ │ ┌────┴────┐ │ │ ▼ ▼ Success Failure │ │ │ ▼ │ ┌─────────┐ │ │ Requeue │ │ │ + Delay │ │ └────┬────┘ │ │ │ Max retries? │ │ │ ┌────┴────┐ │ │ Yes │ No │ ▼ ▼ │ DLQ Retry Queue │ │ └────┴─> Delivery Log ``` ### Worker Pool - **Multiple workers**: Parallel delivery processing - **Rate limiting**: Prevents overwhelming your endpoint - **Timeout handling**: 30-second max per request --- ## Dead Letter Queue (DLQ) After exhausting all retries, failed deliveries move to the Dead Letter Queue for manual intervention. ### What Goes to DLQ? - Deliveries that failed all retry attempts - Events that repeatedly timeout - Persistent 5xx errors from your endpoint ### Manual Retry You can manually retry deliveries via API or UI: **Via API:** ```bash curl -X PATCH https://api-sandbox.fndev.net/api/v1/webhooks/delivery-logs/del_xyz789/retry \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` **Response:** ```json { "metadata": { "timestamp": "2026-01-15T11:00:00Z" }, "result": { "job_id": "job_abc123" } } ``` **Via UI:** ![Delivery Log Retry](../images/delivery-log-retry.webp) This creates a new delivery job that you can track in delivery logs. --- ## Automatic Deactivation To prevent infinite failed delivery attempts, webhooks are automatically deactivated under certain conditions: ### Deactivation Policy - **Trigger**: 7 consecutive days of failed deliveries - **Action**: Webhook status changes from `active` to `inactive` - **Notification**: Email sent to `notificationEmail` (if configured) ### What Happens When Deactivated? - ❌ No new delivery attempts - ❌ Events are not queued - ✅ Webhook configuration preserved - ✅ Historical delivery logs remain accessible ### Reactivation Update webhook status to `active` after fixing endpoint issues: ```bash curl -X PUT https://api-sandbox.fndev.net/api/v1/webhooks/wh_abc123 \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "status": "active" }' ``` > **Important**: Fix the underlying issues before reactivating. Repeated deactivations may result in permanent suspension. --- ## Delivery Logging Every delivery attempt is logged with complete details: ### Log Contents ```json { "deliveryId": "del_xyz789", "webhookId": "wh_abc123", "workOrderId": 12345, "eventName": "workorder.status.published", "deliveryStatus": 200, "deliveryAttempt": 1, "requestUrl": "https://your-endpoint.com/webhooks", "requestMethod": "POST", "requestHeaders": { "content-type": "application/json", "x-fn-signature": "sha256=...", "x-fn-webhook-id": "wh_abc123" }, "requestBody": "{...full payload...}", "responseStatus": 200, "responseHeaders": { "content-type": "text/plain" }, "responseBody": "OK", "responseTime": 145, "createdAt": "2026-01-15T10:30:00Z" } ``` ### Accessing Logs Via API: ```bash # List delivery logs curl -X GET "https://api-sandbox.fndev.net/api/v1/webhooks/delivery-logs?webhookId=wh_abc123" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" # Get specific delivery details curl -X GET https://api-sandbox.fndev.net/api/v1/webhooks/delivery-logs/del_xyz789 \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` Via UI: - Navigate to [Webhooks Dashboard](https://ui-sandbox.fndev.net/integrations/webhooks) - Select your webhook - Click "Delivery Logs" tab ![Delivery Logs Dashboard](../images/delivery-logs.webp) [Learn more about monitoring →](/docs/webhooks/guides/monitoring) --- ## Delivery Best Practices ### Respond Quickly (5%) - Increased retry attempts - Slow response times (>2 seconds) - Webhook deactivation ```javascript // Example CloudWatch metric await cloudwatch.putMetricData({ Namespace: 'Webhooks', MetricData: [{ MetricName: 'DeliveryFailures', Value: failureCount, Unit: 'Count', Timestamp: new Date() }] }); ``` --- ## Delivery Guarantees ### At-Least-Once Delivery Field Nation guarantees **at-least-once delivery**: - ✅ Every event will be delivered at least once - ⚠️ Events may be delivered more than once - 🎯 Implement idempotency to handle duplicates ### Ordering Events are delivered **approximately** in order, but strict ordering is not guaranteed: - Events from the same work order usually arrive in sequence - Network latency and retries can cause reordering - Don't rely on event order for critical logic ### Example Handling Order-Sensitive Events ```javascript async function processWorkOrderEvent(payload) { const { workOrderId, timestamp, data } = payload; // Fetch current work order state const currentState = await getWorkOrder(workOrderId); // Check if event is stale (older than current state) if (new Date(timestamp) < new Date(currentState.updatedAt)) { console.log(`Stale event ignored: ${payload.eventId}`); return; } // Process the event await updateWorkOrder(workOrderId, data); } ``` --- ## Troubleshooting Delivery Issues ### Deliveries Not Arriving? Ensure webhook is `active`: ```bash curl -X GET https://api-sandbox.fndev.net/api/v1/webhooks/wh_abc123 \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` Test publicly accessible: ```bash curl -X POST https://your-endpoint.com/webhooks \ -H "Content-Type: application/json" \ -d '{"test": "data"}' ``` Check for error patterns: ```bash curl -X GET "https://api-sandbox.fndev.net/api/v1/webhooks/delivery-logs?deliveryStatus=500" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` Ensure valid HTTPS certificate: ```bash curl -v https://your-endpoint.com ``` Look for SSL verification errors. ### Common Delivery Errors | Error | Cause | Solution | |-------|-------|----------| | **Connection timeout** | Endpoint slow to respond | Optimize response time, use async processing | | **SSL certificate error** | Invalid/expired certificate | Renew SSL certificate | | **DNS resolution failed** | Domain doesn't resolve | Check DNS configuration | | **Connection refused** | Server not listening on port | Verify server is running, check firewall | | **502 Bad Gateway** | Reverse proxy misconfiguration | Check nginx/load balancer config | [Complete troubleshooting guide →](/docs/webhooks/troubleshooting/delivery-failures) --- --- ### Events URL: /docs/webhooks/concepts/events ## Complete Event Catalog Events are organized below to follow the natural workflow of a work order in Field Nation — from creation through completion and payment. ### Creation workorder.created **Triggered when**: A new work order is created in the system. > [INFO] **Typical Use Cases** - Log new work orders in your database - Trigger initial workflows or notifications - Auto-assign to preferred providers based on criteria ```json title="event.params" { "title": "Router Installation - Site 42", "client": { "id": 100 }, "location": { "city": "Minneapolis", "state": "MN", "zip": "55401", "country": "US" }, "schedule": { "service_window": { "mode": "exact", "start": { "utc": "2026-01-20 14:00:00" }, "end": { "utc": "2026-01-20 18:00:00" } } }, "pay": { "type": "fixed", "base": { "amount": 250.00 } }, "tasks": { "results": [] }, "custom_fields": { "results": [] }, "work_order_id": 10001, "status_id": 1 } ``` > [INFO] `workorder.created` has the largest `event.params` — it includes the full creation payload (location, schedule, pay, tasks, custom fields, qualifications, and more). Fields vary based on what the buyer fills in at creation time. workorder.status.draft **Triggered when**: Work order status changes to **Draft**. **Description**: Initial state when a work order is created but not yet published or routed. ```json title="event.params" { "status_id": 1, "work_order_id": 10001 } ``` ### Publishing & Routing After creation, a work order is either published to the marketplace for any provider to request, or routed directly to specific providers. workorder.status.published **Triggered when**: Work order status changes to **Published**. **Description**: Work order is published to the marketplace and visible to providers. > [INFO] **Typical Use Cases** - Notify provider network - Start SLA timers - Update dispatch boards ```json title="event.params" { "status_id": 2, "work_order_id": 10001 } ``` workorder.routed **Triggered when**: A work order is routed to specific provider(s). > [INFO] **Typical Use Cases** - Notify selected providers - Track routing patterns - Update dispatch boards ```json title="event.params" { "work_order_id": 10001, "routed_ids": [557, 558], "qualifications_version": 1 } ``` workorder.status.routed **Triggered when**: Work order status changes to **Routed**. **Description**: Work order has been routed to specific provider(s). ```json title="event.params" { "status_id": 9, "work_order_id": 10001 } ``` ### Provider Response Once a work order is published or routed, providers can respond — requesting the job, confirming a route, or declining. workorder.requested **Triggered when**: A provider requests assignment to a published work order. > [INFO] **Typical Use Cases** - Notify managers of provider interest - Auto-approve based on provider rating/history - Track request patterns ```json title="event.params" { "work_order_id": 10001, "request": { "id": 48, "counter": false, "active": true, "notes": "", "created": { "utc": "2026-04-24 00:08:17", "local": { "date": "2026-04-23", "time": "19:08:17" } }, "eta": { "start": { "utc": "2026-04-24 15:00:00", "local": { "date": "2026-04-24", "time": "10:00:00" } }, "end": { "utc": "", "local": { "date": "", "time": "" } }, "hour_estimate": 2 }, "technician": { "id": 1001, "first_name": "Jane", "last_name": "Doe", "thumbnail": "https://cdn.example.com/profile/1001.png" }, "actions": ["delete", "edit"], "pay": { "range": { "min": 0, "max": 0 } } }, "qualifications_version": 1, "acting_user_id": 0, "notes": "", "withdrawDuration": false, "expanded": true, "eta": { "user": { "id": 0 }, "task": { "id": 10001 }, "start": { "local": { "date": "2026-04-24", "time": "10:00:00" }, "utc": "2026-04-24 15:00:00" }, "end": { "local": { "date": "2026-04-24", "time": "12:00:00" }, "utc": "2026-04-24 17:00:00" }, "hour_estimate": 2, "notes": "", "status": { "name": "unconfirmed", "updated": { "utc": "", "local": { "date": "", "time": "" } } }, "mode": "exact", "actions": [], "validation": [] }, "bundle": [], "providerOptions": [], "user": { "id": 0 }, "schedule": { "work_order_id": 10001 } } ``` workorder.declined **Triggered when**: A provider declines a work order route. > **Typical Use Cases** - Alert managers to find a replacement - Re-route to other providers - Track decline reasons ```json title="event.params" { "user_id": 1001, "reason_id": 2, "reason_text": "", "decline_time": "2026-04-25 14:00:00" } ``` workorder.undeclined **Triggered when**: A previously declined route is reversed. > [INFO] **Typical Use Cases** - Update provider availability - Re-evaluate assignment - Log status reversals ```json title="event.params" { "user_id": 1001 } ``` ### Assignment After a provider requests or confirms, the work order is assigned. An assignment can also be cancelled. workorder.status.assigned **Triggered when**: Work order status changes to **Assigned**. **Description**: Work order has been assigned to a provider. > [INFO] **Typical Use Cases** - Notify provider of assignment - Update dispatch board - Sync to scheduling systems - Start work order timeline ```json title="event.params" { "status_id": 3, "work_order_id": 10001, "assignee": { "user": { "id": 1001 } } } ``` workorder.status.confirmed **Triggered when**: Work order status changes to **Confirmed**. **Description**: The assigned provider has confirmed the work order. ```json title="event.params" { "work_order_id": 10001, "bundle_id": 0, "user": { "id": 1001 }, "hour_estimate": 1, "notes": "", "has_require_gps_action": true, "require_gps": true, "require_ontime": true, "schedule": { "work_order_id": 10001, "service_window": { "mode": "exact", "start": { "local": { "date": "2026-04-25", "time": "09:00:00" }, "utc": "2026-04-25 14:00:00" }, "end": { "local": { "date": "", "time": "" }, "utc": "" } }, "time_zone": { "id": 4, "name": "America/Chicago", "offset": 0, "short": "CDT" }, "status_id": 2, "company_id": 10000, "today_tomorrow": "Today", "est_labor_hours": 1, "updated": { "utc": "2026-04-25 14:00:00", "local": { "date": "2026-04-25", "time": "09:00:00" }, "by": { "id": 1001, "name": "Jane Doe" } }, "schedule_note": "", "schedule_option": 0, "apply_arrival_window": true, "requests": { "metadata": { "total": 0, "per_page": 0, "page": 1, "pages": 1 }, "results": [], "actions": [] }, "correlation_id": "example_correlation_id", "actions": [] }, "start": { "local": { "date": "2026-04-25", "time": "09:00:00" }, "utc": null }, "end": { "local": { "date": "", "time": "" }, "utc": null }, "eta": { "status": { "name": "confirmed" }, "start": { "utc": "2026-04-25 14:00:00" }, "end": { "utc": "2026-04-25 15:00:00" } }, "assignee": { "user": { "id": 1001 } } } ``` workorder.status.assigned_cancelled **Triggered when**: Work order status changes to **Assigned Cancelled**. **Description**: An assigned work order's assignment has been cancelled. The work order returns to the pool. ```json title="event.params" { "cancel_time": "2026-04-25 14:00:00", "cancel_reason_id": 28, "cancel_reason": "Provider had a scheduling conflict." } ``` ### In-Progress Work While a work order is assigned, several events can fire as the provider travels to the site, performs tasks, communicates, and updates the work order. #### Field Activity workorder.status.on_my_way **Triggered when**: Provider clicks the "On My Way" button. **Description**: Provider is en route to the work location. Includes GPS coordinates and distance to site. > [INFO] **Typical Use Cases** - Notify buyer of provider ETA - Track travel time and distance - Update real-time dashboards ```json title="event.params" { "coords": { "latitude": 44.9778, "longitude": -93.2650, "location_service_enabled": true }, "onmyway_date": "2026-04-25 14:00:00", "status": { "name": "onmyway" }, "assignee": { "user": { "id": 1001 } }, "eta": { "condition": { "distance": 3335 } } } ``` workorder.status.checked_in **Triggered when**: Work order status changes to **Checked In**. **Description**: Provider has checked in to the work order. > [INFO] **Typical Use Cases** - Log start time - Start work timer - Notify stakeholders work has begun ```json title="event.params" { "id": 10001, "in": { "created": { "utc": "2026-04-25 14:00:00", "local": { "date": "2026-04-25", "time": "09:00:00" } }, "coords": { "latitude": 44.9778, "longitude": -93.2650 } }, "service_window": { "mode": "between", "start": { "local": { "date": "2026-04-25", "time": "09:00:00" }, "utc": "2026-04-25 14:00:00" }, "end": { "local": { "date": "2026-04-26", "time": "17:00:00" }, "utc": "2026-04-26 22:00:00" } }, "assignee": { "user": { "id": 1001 } } } ``` workorder.status.checked_out **Triggered when**: Work order status changes to **Checked Out**. **Description**: Provider has checked out from the work order. > [INFO] **Typical Use Cases** - Calculate work duration - Trigger documentation review - Update time tracking systems ```json title="event.params" { "user_id": 1001, "id": 10001, "out": { "created": { "utc": "2026-04-25 15:00:00", "local": { "date": "2026-04-25", "time": "10:00:00" } }, "coords": { "latitude": 44.9778, "longitude": -93.2650 } }, "devices": 0, "hoursLogged": 1.5, "providerEmail": "provider@example.com", "hoursLoggedOld": 0, "woStartTime": "2026-04-25 09:00:00", "assignee": { "user": { "id": 1001 } } } ``` #### Tasks & Communication workorder.task_completed **Triggered when**: A task within a work order is marked complete. > [INFO] **Typical Use Cases** - Track task-level progress - Calculate completion percentage - Trigger next-step workflows ```json title="event.params" { "task": { "id": 10001, "work_order_id": 10001, "description": "Set start time", "descriptions": { "task": "Set start time", "first": "", "second": "", "third": "", "fourth": "" }, "label": "Set Start Time", "alerts": [], "group": { "label": "Pre visit", "id": "prep" }, "type": { "id": 1, "key": "confirmassignment", "name": "Set Start Time" }, "created": { "utc": "2026-05-12 14:39:19", "local": { "date": "2026-05-12", "time": "09:39:19" } }, "author": { "id": 1001, "first_name": "Jane", "last_name": "Doe" }, "status": "complete", "eta": { "start": { "utc": "2026-04-25 14:00:00", "local": { "date": "2026-04-25", "time": "09:00:00" } }, "end": { "utc": "", "local": { "date": "", "time": "" } }, "hour_estimate": 1, "notes": "" }, "actions": ["complete"] }, "work_order_id": 10001 } ``` workorder.task_incomplete **Triggered when**: A task is marked as incomplete. > **Typical Use Cases** - Alert managers to incomplete tasks - Adjust work order timeline - Track quality issues ```json title="event.params" { "task": { "id": 10002, "work_order_id": 10001, "description": "Check in", "descriptions": { "task": "Check in", "first": "", "second": "", "third": "", "fourth": "" }, "label": "Check in", "alerts": [], "group": { "label": "On site", "id": "onsite" }, "type": { "id": 3, "key": "checkin", "name": "Check in" }, "created": { "utc": "2026-05-14 23:07:47", "local": { "date": "2026-05-14", "time": "19:07:47" } }, "author": { "id": 0, "first_name": "", "last_name": "" }, "status": "incomplete", "time_log": [], "actions": ["reorder", "alerts"] } } ``` workorder.message_posted **Triggered when**: A message is posted or replied to on a work order. > [INFO] **Typical Use Cases** - Send email/SMS notifications - Log communication history - Alert on specific keywords ```json title="event.params" { "message": { "from": { "id": 1001, "name": "Jane Doe", "thumbnail": "https://cdn.example.com/profile/1001.png", "hideWoManager": false, "msgLink": "/workorders/10001/messages", "role": "Provider" }, "to": { "id": 1002, "name": "John Smith", "thumbnail": "https://cdn.example.com/profile/1002.png", "role": "Provider" }, "role": "Provider", "msg_id": "10001", "parent_id": "0", "work_order_status": "7", "read": true, "created": { "utc": "2026-04-25 14:00:00", "timezone": "America/Chicago", "local": { "date": "2026-04-25", "time": "09:00:00" } }, "message": "Sample message content.", "problem": [], "actions": ["add", "canReply"], "replies": [], "sharedText": " John Smith (Assigned provider), Example Company " }, "id": 10001 } ``` workorder.provider_upload **Triggered when**: A provider uploads a document. > [INFO] **Typical Use Cases** - Archive documents in your system - Trigger review workflows - Notify stakeholders of completion evidence ```json title="event.params" { "id": 10001, "attachment": { "upload_unique_id": "v1:example0000000000000000000000000000000000:00000000000000", "id": 10001, "author": { "id": 1001, "first_name": "Jane", "last_name": "Doe", "thumbnail": "https://cdn.example.com/profile/1001.png", "phone": "+10005550100" }, "reviewer": { "id": 0, "first_name": "", "last_name": "" }, "file": { "name": "expanded.png", "mime": "image/png", "size_bytes": 104829, "thumbnail": "https://cdn.example.com/workorder/10001-thumb.png", "link": "https://cdn.example.com/workorder/10001.png", "description": "expanded.png", "type": "file", "icon": "icon-picture", "preview": "https://cdn.example.com/workorder/10001.png" }, "created": { "utc": "2026-04-25 14:00:00", "local": { "date": "2026-04-25", "time": "09:00:00" } }, "time_zone": { "name": "America/New_York", "short": "EDT" }, "notes": "", "status": "pending", "status_description": "", "task": { "id": 10001, "type": { "id": 6 } }, "reviewed": { "utc": "", "local": { "date": "", "time": "" } }, "expense": { "id": 0 }, "show_before_assignment": false, "actions": ["notes", "approve", "deny", "view", "delete", "edit"], "folder_id": 10001, "workorder_id": 10001 } } ``` #### Work Order Updates workorder.custom_field_value_updated **Triggered when**: A custom field value is changed. > [INFO] **Typical Use Cases** - Sync custom data to external systems - Trigger workflows based on field values - Track data changes ```json title="event.params" { "field": { "id": 10001, "name": "Custom Field Name", "tip": "", "type": "numeric", "role": "assigned_provider", "dependency": { "id": 0, "value": "", "operator": "equals" }, "used_in_finance": false, "options": [], "flags": ["seen_by_provider"], "value": "", "category": "General", "order": 29, "actions": ["edit"] }, "value": "" } ``` workorder.schedule_updated **Triggered when**: The work order schedule is modified. > [INFO] **Typical Use Cases** - Update calendar systems - Notify providers and buyers of changes - Track schedule volatility ```json title="event.params" { "time_zone": { "id": 4 }, "service_window": { "mode": "exact", "start": { "local": { "date": "2026-04-24", "time": "06:00" } }, "end": { "local": { "date": "2026-04-24", "time": "" } } }, "require_ontime": true, "schedule_option": 0, "schedule_note": "", "new": { "Schedule": { "mode": "exact", "start": "2026-04-24 06:00:00", "startTime": "2026-04-24T06:00:00-05:00", "endTime": "", "end": "", "time_zone": "America/Chicago", "schedule_note": null, "schedule_option": 0 } }, "old": { "Schedule": { "mode": "exact", "start": "", "startTime": "", "endTime": "", "end": "", "time_zone": "", "schedule_note": null, "schedule_option": null } } } ``` workorder.tag_added **Triggered when**: A tag is added to a work order. > [INFO] **Typical Use Cases** - Categorize work orders dynamically - Trigger tag-based routing - Track work order classifications ```json title="event.params" { "id": 1001, "custom_tag": true } ``` workorder.tag_removed **Triggered when**: A tag is removed from a work order. > [INFO] **Typical Use Cases** - Update categorizations - Log tag history - Sync taxonomy changes ```json title="event.params" { "id": 1001, "custom_tag": true } ``` workorder.part_updated **Triggered when**: Parts/materials on a work order are updated. **Description**: Inventory or parts list has been modified. > [INFO] **Typical Use Cases** - Sync inventory systems - Track parts usage - Update cost estimates - Manage supply chain ```json title="event.params" { "work_order_id": 10001, "part_id": 10001, "external_id": "EXT-001", "parts": { "results": { "metadata": { "pages": 1, "page": 1 }, "results": { "id": 10001, "external_id": "EXT-001", "description": "Replacement component for installation.", "external_number": "1", "usage_code": null, "note": "", "incoming_tracking": { "tracking_id": "1Z999AA10123456784", "carrier_name": "fedex" }, "return_tracking": { "tracking_id": "", "carrier_name": "", "not_returned_reason": null }, "return_policy": { "is_returnable": false, "return_instruction": "Leave part on site" }, "added_by_provider": false } } } } ``` #### Issues & Risks workorder.status.at_risk **Triggered when**: Work order status changes to **At Risk**. **Description**: Work order is flagged as at risk of not completing on time. > **Typical Use Cases** - Alert managers - Escalate to senior technicians - Prepare backup plans ```json title="event.params" { "status": { "id": 0, "code": "at_risk", "estimated_delay": 0, "reported_date": null, "problem_id": null, "description": null }, "id": 10001, "assignee": { "user": { "id": 1001 } } } ``` workorder.status.delayed **Triggered when**: Provider clicks the "Running Late" button from the Assigned work order. **Description**: Work order is delayed beyond scheduled time. Includes estimated delay in seconds and a description of the delay reason. > **Typical Use Cases** - Notify buyers of delay - Adjust schedules - Track on-time performance ```json title="event.params" { "status": { "id": 2, "code": "delayed", "estimated_delay": 3600, "reported_date": "2026-04-25 14:00:00", "problem_id": 10001, "description": "Provider is experiencing an unexpected delay." }, "id": 10001, "assignee": { "user": { "id": 1001 } }, "eta": { "condition": { "distance": 0 } } } ``` workorder.problem_reported **Triggered when**: A problem is reported or reopened on a work order. **Description**: Issue flagged that requires attention or resolution. > **Typical Use Cases** - Alert quality assurance teams - Escalate to managers - Track problem frequency - Trigger resolution workflows ```json title="event.params" { "type": { "id": 73, "name": "How to/Technical assistance", "other": "How to/Technical assistance" }, "comments": "Provider needs technical assistance onsite. Please review and coordinate.", "resolution": { "status": "open" }, "author": { "id": 1001, "first_name": "Jane", "last_name": "Doe" }, "created": { "utc": "2026-04-25 10:00:00", "local": { "date": "2026-04-25", "time": "05:00:00" } } } ``` workorder.problem_resolved **Triggered when**: A reported problem is resolved. **Description**: Previously reported issue has been addressed. > [INFO] **Typical Use Cases** - Close problem tickets - Calculate resolution times - Resume normal workflows - Update quality metrics ```json title="event.params" { "type": { "id": 73, "name": "How to/Technical assistance", "other": "How to/Technical assistance" }, "comments": "Provider needs technical assistance onsite. Please review and coordinate.", "resolution": { "status": "resolved", "at": { "utc": "2026-04-25 14:00:00", "date": "2026-04-25", "time": "09:00:00" }, "by": { "id": -2, "first_name": "Field Nation", "last_name": "Support" } }, "author": { "id": 1001, "first_name": "Jane", "last_name": "Doe" }, "created": { "utc": "2026-04-25 10:00:00", "local": { "date": "2026-04-25", "time": "05:00:00" } } } ``` ### Completion & Approval Once the provider finishes work, the work order moves through approval and payment. workorder.status.work_done **Triggered when**: Work order status changes to **Work Done**. **Description**: Provider has completed the work and submitted for approval. > [INFO] **Typical Use Cases** - Trigger approval workflows - Notify managers and buyers - Calculate completion metrics ```json title="event.params" { "status_id": 4, "work_order_id": 10001, "assignee": { "user": { "id": 1001 } }, "eta": { "condition": { "distance": "0" } } } ``` workorder.status.approved **Triggered when**: Work order status changes to **Approved**. **Description**: Buyer has approved the completed work. > [INFO] **Typical Use Cases** - Generate invoices - Release payments - Close work orders in external systems - Calculate approval times ```json title="event.params" { "status_id": 5, "work_order_id": 10001, "assignee": { "user": { "id": 1001 } }, "eta": { "condition": { "distance": "0" } } } ``` workorder.status.paid **Triggered when**: Work order status changes to **Paid**. **Description**: Provider has been paid for the work order. > [INFO] **Typical Use Cases** - Update accounting systems - Archive completed work orders - Calculate payment cycles - Generate financial reports ```json title="event.params" { "status_id": 6, "work_order_id": 10001, "assignee": { "user": { "id": 1001 } } } ``` ### Cancellation & Other These events can occur at various points in the workflow and represent terminal or escape states. workorder.status.cancelled **Triggered when**: Work order status changes to **Cancelled**. **Description**: Work order has been cancelled before completion. > **Typical Use Cases** - Update availability calendars - Notify affected parties - Track cancellation reasons - Calculate cancellation rates ```json title="event.params" { "cancel_reason": "Site not ready for installation", "cancel_request_not_charge": false, "message_to_provider": "" } ``` workorder.status.deleted **Triggered when**: Work order status changes to **Deleted**. **Description**: Work order has been deleted from the system. ```json title="event.params" { "status_id": 10, "work_order_id": 10001 } ``` workorder.status.postponed **Triggered when**: Buyer clicks "Add Hold" button from the Work Order is **Assigned** status. **Description**: Work order has been placed on hold. Contains the hold reason and type. > [INFO] **Typical Use Cases** - Update schedules - Notify providers of hold status - Track postponement patterns ```json title="event.params" { "reason": "Work order is on hold pending further review.", "type": { "id": "22" } } ``` --- ### Work Order Lifecycle > Nodes show event names without the `workorder.` prefix for brevity (e.g., `workorder.status.published`). Dashed arrows indicate escape states that can exit the main flow. ```mermaid --- config: flowchart: curve: linear --- flowchart TD A(status.draft) -->|Published| B(status.published) A -->|Routed| C(status.routed) B -->|Assigned| D(status.assigned) C -->|Assigned| D D -->|Confirmed| E(status.confirmed) E -->|En route| F(status.on_my_way) F -->|Arrived| G(status.checked_in) G -->|Finished| H(status.checked_out) H -->|Submitted| I(status.work_done) I -->|Approved| J(status.approved) J -->|Paid| K(status.paid) A -.->|Deleted| L(status.deleted) D -.->|Unassigned| M(status.assigned_cancelled) D -.->|Postponed| N(status.postponed) A & B & C & D -.->|Cancelled| O(status.cancelled) ``` --- ## Event Filtering When creating a webhook, you can subscribe to specific events to reduce noise. ### Configuration Examples Subscribe to everything for full synchronization. ```json { "events": [ "workorder.created", "workorder.status.draft", "workorder.status.published", "workorder.routed", "workorder.status.routed", "workorder.requested", "workorder.declined", "workorder.undeclined", "workorder.status.assigned", "workorder.status.confirmed", "workorder.status.assigned_cancelled", "workorder.status.on_my_way", "workorder.status.checked_in", "workorder.status.checked_out", "workorder.task_completed", "workorder.task_incomplete", "workorder.message_posted", "workorder.provider_upload", "workorder.custom_field_value_updated", "workorder.schedule_updated", "workorder.tag_added", "workorder.tag_removed", "workorder.part_updated", "workorder.status.at_risk", "workorder.status.delayed", "workorder.problem_reported", "workorder.problem_resolved", "workorder.status.work_done", "workorder.status.approved", "workorder.status.paid", "workorder.status.cancelled", "workorder.status.deleted", "workorder.status.postponed" ] } ``` Track work order creation and how it reaches the provider pool. ```json { "events": [ "workorder.created", "workorder.status.published", "workorder.routed", "workorder.status.routed" ] } ``` Monitor assignment status changes and confirmation. ```json { "events": [ "workorder.status.assigned", "workorder.status.confirmed", "workorder.status.assigned_cancelled" ] } ``` Track on-site operations, task progress, and in-progress updates. ```json { "events": [ "workorder.status.on_my_way", "workorder.status.checked_in", "workorder.task_completed", "workorder.message_posted", "workorder.status.checked_out" ] } ``` Trigger downstream workflows when work is done and payment released. ```json { "events": [ "workorder.status.work_done", "workorder.status.approved", "workorder.status.paid" ] } ``` Handle cancellations, postponements, and deletions across any phase. ```json { "events": [ "workorder.status.cancelled", "workorder.status.assigned_cancelled", "workorder.status.postponed", "workorder.status.deleted" ] } ``` Minimal integration for high-value signals. ```json { "events": [ "workorder.status.published", "workorder.status.assigned", "workorder.status.work_done", "workorder.status.approved" ] } ``` --- ## Best Practices ### Start Small, Expand Later ### Begin with Core Events Subscribe to 4-6 critical events initially: - `workorder.created` - `workorder.status.assigned` - `workorder.status.work_done` - `workorder.status.approved` ### Test Thoroughly Verify your system handles events correctly before adding more. ### Add Events Incrementally Expand to additional events as you build new features. ### Filter Strategically Don't subscribe to events you won't use. | Strategy | Description | Impact | | :--- | :--- | :--- | | ❌ **Bad** | Subscribe to all 33 events "just in case" | Increases processing overhead and log volume. | | ✅ **Good** | Subscribe only to events your system processes | Cleaner logs, less noise, lower server load. | ### Document Your Event Handling Maintain internal documentation mapping events to your handlers: | Event | Handler | Purpose | | :--- | :--- | :--- | | `workorder.status.published` | `publishHandler()` | Notify provider network | | `workorder.status.assigned` | `assignHandler()` | Update dispatch board | | `workorder.status.work_done` | `completeHandler()` | Trigger approval flow | --- ### Payload structure URL: /docs/webhooks/concepts/payload-structure ## HTTP Request Structure Field Nation sends webhooks as HTTP POST or PUT requests (configurable) to your endpoint: ```http 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": { "status_id": 2, "work_order_id": 10001 } }, "timestamp": "2026-01-20T08:55:58-06:00", "triggered_by_user": { "id": 1068 }, "workorder": { "id": 10001, "title": "Router Installation - Site 42" } } ``` --- ## Standard Headers Every webhook request includes these headers: HMAC-SHA256 signature for verifying webhook authenticity. Format: sha256=<hex_digest>, required: true }, "X-FN-Webhook-Id": { type: "string", description: "Unique identifier of the webhook configuration (UUID format)", required: true }, "X-FN-Delivery-Id": { type: "string", description: <>Unique identifier for this delivery attempt. Format: uuid:attempt_number. Use for idempotency, required: true }, "X-FN-Event-Trace-Id": { type: "string", description: "Trace ID for correlating this event across systems (UUID format)", required: true }, "X-FN-Request-Timestamp": { type: "string", description: "Unix timestamp in milliseconds when the webhook was sent", required: true }, "Content-Type": { type: "string", description: <>Always application/json, required: true } }} /> ### Custom Headers You can configure additional headers when creating the webhook: ```json { "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 Every webhook delivery shares the same **envelope**. The payload always includes these four top-level keys: event, timestamp, triggered_by_user, and workorder. The table below documents both those top-level fields and selected nested properties using dotted notation such as event.name and workorder.id. ### Webhook Envelope Event metadata. Contains name (the event type string, e.g. workorder.status.published) and params (event-specific data — varies per event, see Event Params by Type), required: true }, "event.name": { type: "string", description: <>The event type identifier. One of the 33 supported event names (e.g. workorder.status.assigned), required: true }, "event.params": { type: "object", description: <>Event-specific parameters. Shape varies per event — status events include status_id and work_order_id, activity events include the full domain object (message, task, attachment, etc.), required: true }, "timestamp": { type: "string", description: <>ISO 8601 timestamp with timezone offset when the event occurred. Example: 2026-01-20T08:55:58-06:00, required: true }, "triggered_by_user": { type: "object", description: <>The user who caused this event. Contains id (integer user ID), required: true }, "triggered_by_user.id": { type: "integer", description: "Field Nation user ID of the person or system that triggered the event", required: true }, "workorder": { type: "object", description: <>Summary of the affected work order. Contains id and title. For the full work order object, use the REST API with the id from this field, required: true }, "workorder.id": { type: "integer", description: <>Work order ID. Use this to fetch the full work order via GET /api/rest/v2/workorders/{'{id}'}, required: true }, "workorder.title": { type: "string", description: "Work order title at the time the event fired", required: true } }} /> > [INFO] **The `workorder` object shown in examples on this page is abbreviated for readability.** In production, the webhook delivers the full work order — the same object returned by the [REST API](/docs/rest-api/work-orders/overview), including status, location, schedule, pay, assignee, custom fields, and more. > [INFO] **Need a different payload shape?** You can transform the webhook payload, filter fields, and build dynamic URLs using [Payload Customization](/docs/webhooks/guides/payload-customization) with JSONata expressions — no middleware required. ### Example Envelope ```json title="workorder.status.published" { "event": { "name": "workorder.status.published", "params": { "status_id": 2, "work_order_id": 10001 } }, "timestamp": "2026-01-20T08:55:58-06:00", "triggered_by_user": { "id": 1068 }, "workorder": { "id": 10001, "title": "Router Installation - Site 42" } } ``` --- ## Event Params by Type The `event.params` shape varies by event. Status change events carry minimal metadata, while activity events carry the full domain object. ### Status Change Events Most `workorder.status.*` events share a simple structure: ```json title="event.params for status events" { "status_id": 2, "work_order_id": 10001 } ``` Events with this shape: `workorder.status.draft`, `workorder.status.published`, `workorder.status.routed`, `workorder.status.deleted`. Some status events carry additional context: > [INFO] `event.params` keys vary by status event. Some examples below omit `status_id` and/or `work_order_id`. When those fields are not present, use the webhook envelope's `workorder.id` together with `event.name` to identify the work order and status event. ```json { "status_id": 3, "work_order_id": 10001, "assignee": { "user": { "id": 1001 } } } ``` ```json { "cancel_time": "2026-04-25 14:00:00", "cancel_reason_id": 28, "cancel_reason": "Provider had a scheduling conflict." } ``` ```json { "status_id": 4, "work_order_id": 10001, "assignee": { "user": { "id": 1001 } }, "eta": { "condition": { "distance": "0" } } } ``` ```json { "status_id": 5, "work_order_id": 10001, "assignee": { "user": { "id": 1001 } }, "eta": { "condition": { "distance": "0" } } } ``` ```json { "status_id": 6, "work_order_id": 10001, "assignee": { "user": { "id": 1001 } } } ``` ```json { "cancel_reason": "Site not ready for installation", "cancel_request_not_charge": false, "message_to_provider": "" } ``` ```json { "reason": "Work order is on hold pending further review.", "type": { "id": "22" } } ``` ```json { "id": 10001, "in": { "created": { "utc": "2026-04-25 14:00:00", "local": { "date": "2026-04-25", "time": "09:00:00" } }, "coords": { "latitude": 44.9778, "longitude": -93.2650 } }, "service_window": { "mode": "between", "start": { "local": { "date": "2026-04-25", "time": "09:00:00" }, "utc": "2026-04-25 14:00:00" }, "end": { "local": { "date": "2026-04-26", "time": "17:00:00" }, "utc": "2026-04-26 22:00:00" } }, "assignee": { "user": { "id": 1001 } } } ``` ```json { "user_id": 1001, "id": 10001, "out": { "created": { "utc": "2026-04-25 15:00:00", "local": { "date": "2026-04-25", "time": "10:00:00" } }, "coords": { "latitude": 44.9778, "longitude": -93.2650 } }, "devices": 0, "hoursLogged": 1.5, "providerEmail": "provider@example.com", "hoursLoggedOld": 0, "woStartTime": "2026-04-25 09:00:00", "assignee": { "user": { "id": 1001 } } } ``` ### ETA-Based Events Events `workorder.status.confirmed`, `workorder.status.on_my_way`, `workorder.status.at_risk`, and `workorder.status.delayed` relate to provider ETA and arrival status: ```json { "work_order_id": 10001, "bundle_id": 0, "user": { "id": 1001 }, "hour_estimate": 1, "notes": "", "has_require_gps_action": true, "require_gps": true, "require_ontime": true, "schedule": { "work_order_id": 10001, "service_window": { "mode": "exact", "start": { "local": { "date": "2026-04-25", "time": "09:00:00" }, "utc": "2026-04-25 14:00:00" }, "end": { "local": { "date": "", "time": "" }, "utc": "" } }, "time_zone": { "id": 4, "name": "America/Chicago", "offset": 0, "short": "CDT" }, "status_id": 2, "company_id": 10000, "today_tomorrow": "Today", "est_labor_hours": 1, "updated": { "utc": "2026-04-25 14:00:00", "local": { "date": "2026-04-25", "time": "09:00:00" }, "by": { "id": 1001, "name": "Jane Doe" } }, "schedule_note": "", "schedule_option": 0, "apply_arrival_window": true, "requests": { "metadata": { "total": 0, "per_page": 0, "page": 1, "pages": 1 }, "results": [], "actions": [] }, "correlation_id": "example_correlation_id", "actions": [] }, "start": { "local": { "date": "2026-04-25", "time": "09:00:00" }, "utc": null }, "end": { "local": { "date": "", "time": "" }, "utc": null }, "eta": { "status": { "name": "confirmed" }, "start": { "utc": "2026-04-25 14:00:00" }, "end": { "utc": "2026-04-25 15:00:00" } }, "assignee": { "user": { "id": 1001 } } } ``` ```json { "coords": { "latitude": 44.9778, "longitude": -93.2650, "location_service_enabled": true }, "onmyway_date": "2026-04-25 14:00:00", "status": { "name": "onmyway" }, "assignee": { "user": { "id": 1001 } }, "eta": { "condition": { "distance": 3335 } } } ``` ```json { "status": { "id": 0, "code": "at_risk", "estimated_delay": 0, "reported_date": null, "problem_id": null, "description": null }, "id": 10001, "assignee": { "user": { "id": 1001 } } } ``` ```json { "status": { "id": 2, "code": "delayed", "estimated_delay": 3600, "reported_date": "2026-04-25 14:00:00", "problem_id": 10001, "description": "Provider is experiencing an unexpected delay." }, "id": 10001, "assignee": { "user": { "id": 1001 } }, "eta": { "condition": { "distance": 0 } } } ``` In these examples, the event-specific status value appears in different fields: Confirmed uses `eta.status.name`, On My Way uses `status.name`, and At Risk and Delayed use `status.code`. ### Activity Events Activity events include the full domain object in `event.params`: ```json { "message": { "from": { "id": 1001, "name": "Jane Doe", "thumbnail": "https://cdn.example.com/profile/1001.png", "hideWoManager": false, "msgLink": "/workorders/10001/messages", "role": "Provider" }, "to": { "id": 1002, "name": "John Smith", "thumbnail": "https://cdn.example.com/profile/1002.png", "role": "Provider" }, "role": "Provider", "msg_id": "10001", "parent_id": "0", "work_order_status": "7", "read": true, "created": { "utc": "2026-04-25 14:00:00", "timezone": "America/Chicago", "local": { "date": "2026-04-25", "time": "09:00:00" } }, "message": "Sample message content.", "problem": [], "actions": ["add", "canReply"], "replies": [], "sharedText": " John Smith (Assigned provider), Example Company " }, "id": 10001 } ``` ```json { "id": 10001, "attachment": { "upload_unique_id": "v1:example0000000000000000000000000000000000:00000000000000", "id": 10001, "author": { "id": 1001, "first_name": "Jane", "last_name": "Doe", "thumbnail": "https://cdn.example.com/profile/1001.png", "phone": "+10005550100" }, "reviewer": { "id": 0, "first_name": "", "last_name": "" }, "file": { "name": "expanded.png", "mime": "image/png", "size_bytes": 104829, "thumbnail": "https://cdn.example.com/workorder/10001-thumb.png", "link": "https://cdn.example.com/workorder/10001.png", "description": "expanded.png", "type": "file", "icon": "icon-picture", "preview": "https://cdn.example.com/workorder/10001.png" }, "created": { "utc": "2026-04-25 14:00:00", "local": { "date": "2026-04-25", "time": "09:00:00" } }, "time_zone": { "name": "America/New_York", "short": "EDT" }, "notes": "", "status": "pending", "status_description": "", "task": { "id": 10001, "type": { "id": 6 } }, "reviewed": { "utc": "", "local": { "date": "", "time": "" } }, "expense": { "id": 0 }, "show_before_assignment": false, "actions": ["notes", "approve", "deny", "view", "delete", "edit"], "folder_id": 10001, "workorder_id": 10001 } } ``` ```json { "task": { "id": 10001, "work_order_id": 10001, "description": "Set start time", "descriptions": { "task": "Set start time", "first": "", "second": "", "third": "", "fourth": "" }, "label": "Set Start Time", "alerts": [], "group": { "label": "Pre visit", "id": "prep" }, "type": { "id": 1, "key": "confirmassignment", "name": "Set Start Time" }, "created": { "utc": "2026-05-12 14:39:19", "local": { "date": "2026-05-12", "time": "09:39:19" } }, "author": { "id": 1001, "first_name": "Jane", "last_name": "Doe" }, "status": "complete", "eta": { "start": { "utc": "2026-04-25 14:00:00", "local": { "date": "2026-04-25", "time": "09:00:00" } }, "end": { "utc": "", "local": { "date": "", "time": "" } }, "hour_estimate": 1, "notes": "" }, "actions": ["complete"] }, "work_order_id": 10001 } ``` ```json { "task": { "id": 10002, "work_order_id": 10001, "description": "Check in", "descriptions": { "task": "Check in", "first": "", "second": "", "third": "", "fourth": "" }, "label": "Check in", "alerts": [], "group": { "label": "On site", "id": "onsite" }, "type": { "id": 3, "key": "checkin", "name": "Check in" }, "created": { "utc": "2026-05-14 23:07:47", "local": { "date": "2026-05-14", "time": "19:07:47" } }, "author": { "id": 0, "first_name": "", "last_name": "" }, "status": "incomplete", "time_log": [], "actions": ["reorder", "alerts"] } } ``` ### Other Event Params ```json { "work_order_id": 10001, "routed_ids": [557, 558], "qualifications_version": 1 } ``` ```json { "work_order_id": 10001, "request": { "id": 48, "counter": false, "active": true, "notes": "", "created": { "utc": "2026-04-24 00:08:17", "local": { "date": "2026-04-23", "time": "19:08:17" } }, "eta": { "start": { "utc": "2026-04-24 15:00:00", "local": { "date": "2026-04-24", "time": "10:00:00" } }, "end": { "utc": "", "local": { "date": "", "time": "" } }, "hour_estimate": 2 }, "technician": { "id": 1001, "first_name": "Jane", "last_name": "Doe", "thumbnail": "https://cdn.example.com/profile/1001.png" }, "actions": ["delete", "edit"], "pay": { "range": { "min": 0, "max": 0 } } }, "qualifications_version": 1, "acting_user_id": 0, "notes": "", "withdrawDuration": false, "expanded": true, "eta": { "user": { "id": 0 }, "task": { "id": 10001 }, "start": { "local": { "date": "2026-04-24", "time": "10:00:00" }, "utc": "2026-04-24 15:00:00" }, "end": { "local": { "date": "2026-04-24", "time": "12:00:00" }, "utc": "2026-04-24 17:00:00" }, "hour_estimate": 2, "notes": "", "status": { "name": "unconfirmed", "updated": { "utc": "", "local": { "date": "", "time": "" } } }, "mode": "exact", "actions": [], "validation": [] }, "bundle": [], "providerOptions": [], "user": { "id": 0 }, "schedule": { "work_order_id": 10001 } } ``` ```json { "user_id": 1001, "reason_id": 2, "reason_text": "", "decline_time": "2026-04-25 14:00:00" } ``` ```json { "user_id": 1001 } ``` ```json { "time_zone": { "id": 4 }, "service_window": { "mode": "exact", "start": { "local": { "date": "2026-04-24", "time": "06:00" } }, "end": { "local": { "date": "2026-04-24", "time": "" } } }, "require_ontime": true, "schedule_option": 0, "schedule_note": "", "new": { "Schedule": { "mode": "exact", "start": "2026-04-24 06:00:00", "startTime": "2026-04-24T06:00:00-05:00", "endTime": "", "end": "", "time_zone": "America/Chicago", "schedule_note": null, "schedule_option": 0 } }, "old": { "Schedule": { "mode": "exact", "start": "", "startTime": "", "endTime": "", "end": "", "time_zone": "", "schedule_note": null, "schedule_option": null } } } ``` ```json { "field": { "id": 10001, "name": "Custom Field Name", "tip": "", "type": "numeric", "role": "assigned_provider", "dependency": { "id": 0, "value": "", "operator": "equals" }, "used_in_finance": false, "options": [], "flags": ["seen_by_provider"], "value": "", "category": "General", "order": 29, "actions": ["edit"] }, "value": "" } ``` ```json { "id": 1001, "custom_tag": true } ``` ```json { "work_order_id": 10001, "part_id": 10001, "external_id": "EXT-001", "parts": { "results": { "metadata": { "pages": 1, "page": 1 }, "results": { "id": 10001, "external_id": "EXT-001", "description": "Replacement component for installation.", "external_number": "1", "usage_code": null, "note": "", "incoming_tracking": { "tracking_id": "1Z999AA10123456784", "carrier_name": "fedex" }, "return_tracking": { "tracking_id": "", "carrier_name": "", "not_returned_reason": null }, "return_policy": { "is_returnable": false, "return_instruction": "Leave part on site" }, "added_by_provider": false } } } } ``` ```json { "type": { "id": 73, "name": "How to/Technical assistance", "other": "How to/Technical assistance" }, "comments": "Provider needs technical assistance onsite. Please review and coordinate.", "resolution": { "status": "open" }, "author": { "id": 1001, "first_name": "Jane", "last_name": "Doe" }, "created": { "utc": "2026-04-25 10:00:00", "local": { "date": "2026-04-25", "time": "05:00:00" } } } ``` ```json { "type": { "id": 73, "name": "How to/Technical assistance", "other": "How to/Technical assistance" }, "comments": "Provider needs technical assistance onsite. Please review and coordinate.", "resolution": { "status": "resolved", "at": { "utc": "2026-04-25 14:00:00", "date": "2026-04-25", "time": "09:00:00" }, "by": { "id": -2, "first_name": "Field Nation", "last_name": "Support" } }, "author": { "id": 1001, "first_name": "Jane", "last_name": "Doe" }, "created": { "utc": "2026-04-25 10:00:00", "local": { "date": "2026-04-25", "time": "05:00:00" } } } ``` > [INFO] The `workorder.created` event has the largest `event.params` — it contains the full work order creation payload including location, schedule, pay, tasks, and custom fields. See the [complete event catalog](/docs/webhooks/concepts/events) for details. --- ## Fetching Full Work Order Data In the webhook envelope, the top-level `workorder` field contains the full work order payload — the same object returned by the v2 REST API, including status, location, schedule, pay, assignee, custom fields, and more. The `event.params` object is event-specific; depending on the webhook type, it may include additional references such as `work_order_id` or `title` — the exact fields vary per event. To re-fetch the current state of the work order at any time, call the REST API: ```bash title="Fetch full work order after receiving a webhook" curl -X GET "https://api.fieldnation.com/api/rest/v2/workorders/10001" \ -H "Authorization: Bearer ${FN_ACCESS_TOKEN}" \ -H "Accept: application/json" ``` The response matches the [Work Order REST API schema](/docs/rest-api/work-orders/overview), which includes 100+ fields across these domains: Current status with id and name (e.g. {'{ "id": 2, "name": "Published" }'}), required: true }, "company": { type: "object", description: <>Buyer company — id and name, required: true }, "location": { type: "object", description: <>Service site — city, state, zip, country, geo (lat/lng), required: true }, "schedule": { type: "object", description: <>Service window — service_window, time_zone, eta, required: true }, "pay": { type: "object", description: <>Pay structure — type (fixed/hourly/device/blended), base, additional, required: true }, "manager": { type: "object", description: <>Buyer project manager — id, first_name, last_name }, "assignee": { type: "object | null", description: <>Assigned provider (null if unassigned) — id, first_name, last_name, phone, email }, "custom_fields": { type: "object", description: <>Custom fields configured by the buyer — results[] array with name, value pairs }, "tags": { type: "object", description: <>Work order tags — results[] array with id, name }, "integrations": { type: "object", description: <>External system references — results[] array with id, name (e.g. ServiceNow ticket IDs) } }} /> > [INFO] **Full Schema**: The complete work order schema includes 100+ fields. Refer to the [REST API documentation](/docs/rest-api/work-orders/overview) for the full specification. --- ## Parsing Webhooks ### Essential Parsing Steps ### Read Raw Body **Critical for signature verification** - read the raw request body before parsing: ```javascript 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: ```javascript 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 ```javascript 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 ```javascript // 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 ```javascript // Acknowledge receipt immediately res.status(200).send('OK'); // Process asynchronously processWebhookAsync(payload); ``` --- ## Complete Processing Example ```javascript title="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. > [INFO] **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 ```javascript // 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 ```sql 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); ``` ```javascript 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 HTTP/1.1 200 OK Content-Type: text/plain OK ``` > [INFO] **Response Body**: Field Nation doesn't parse the response body. Any 2xx status code indicates success. ### Response Time - ** **Security**: Never log webhooks in production—they contain sensitive data (PII, financial details, etc.) --- --- ### Webhook lifecycle URL: /docs/webhooks/concepts/webhook-lifecycle ## Webhook States ```mermaid stateDiagram-v2 [*] --> active: Create (default) [*] --> inactive: Create (paused) active --> inactive: Pause deliveries inactive --> active: Resume deliveries active --> archived: Deactivate inactive --> archived: Deactivate archived --> [*]: Permanent (read-only) note right of active Receiving notifications Events queued & delivered end note note right of inactive Paused temporarily Events not delivered end note note right of archived Permanently deactivated Cannot be reactivated end note ``` --- ## Active State **Definition**: Webhook is fully operational and receiving event notifications. ### Characteristics - ✅ Events are queued for delivery - ✅ Delivery attempts made according to retry policy - ✅ Delivery logs generated - ✅ Can be modified (events, URL, etc.) - ✅ Can transition to inactive or archived ### When to Use - **Production webhooks**: Actively processing events - **Monitoring**: Need real-time updates - **Integration running**: System operational ### Creating Active Webhook ```bash 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.status.published"] }' ``` 1. Navigate to [Webhooks Dashboard](https://ui-sandbox.fndev.net/integrations/webhooks) 2. Click "Create New" 3. Fill in configuration 4. Set **Status** to "Active" 5. Click "Create" --- ## Inactive State **Definition**: Webhook is temporarily paused and not receiving events. ### Characteristics - ❌ Events are **not** queued or delivered - ❌ No delivery attempts made - ✅ Configuration preserved - ✅ Historical delivery logs remain accessible - ✅ Can be modified - ✅ Can transition to active or archived ### When to Use - **Maintenance**: Endpoint undergoing updates - **Temporary issues**: Fixing integration problems - **Testing**: Paused during development - **Rate limiting**: Temporarily reduce load ### Common Use Cases #### Maintenance Window ```javascript // Pause webhook during deployment await fetch('https://api-sandbox.fndev.net/api/v1/webhooks/wh_abc123', { method: 'PUT', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ status: 'inactive' }) }); // Perform deployment... // Reactivate after deployment await fetch('https://api-sandbox.fndev.net/api/v1/webhooks/wh_abc123', { method: 'PUT', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ status: 'active' }) }); ``` #### Incident Response ```bash # Quickly pause webhook during incident curl -X PUT https://api-sandbox.fndev.net/api/v1/webhooks/wh_abc123 \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{"status": "inactive"}' ``` ### Pausing vs Deleting > [INFO] **Best Practice**: Use `inactive` for temporary pauses instead of deleting and recreating. This preserves: - Webhook ID and secret - Delivery history - Configuration settings - Change history --- ## Archived State **Definition**: Webhook is permanently deactivated and read-only. ### Characteristics - ❌ Events are **not** delivered - ❌ Cannot be reactivated (permanent) - ❌ Cannot be modified - ✅ Configuration visible (read-only) - ✅ Historical delivery logs preserved - ✅ Appears in webhook list with "archived" badge ### When to Use - **Decommissioned integrations**: Project ended - **Replaced webhooks**: New version created - **Historical record**: Keep audit trail - **Cleanup**: Organize active webhooks ### Archiving a Webhook ```bash curl -X PUT https://api-sandbox.fndev.net/api/v1/webhooks/wh_abc123 \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "status": "archived" }' ``` > **Irreversible**: Once archived, a webhook cannot be reactivated. Create a new webhook if needed. 1. Navigate to webhook details 2. Click "Settings" 3. Change **Status** to "Archived" 4. Confirm action > **Irreversible**: Cannot undo archival. Consider using "Inactive" for temporary pauses. ### Archive vs Delete | Action | Configuration | Delivery Logs | Reversible | |--------|--------------|---------------|------------| | **Archive** | Preserved (read-only) | Preserved | No | | **Delete** | Removed | Preserved | No | > [INFO] **Recommendation**: Archive webhooks instead of deleting to maintain audit trails. --- ## State Transitions ### Valid Transitions ```plaintext active ⟷ inactive ✅ Bidirectional active → archived ✅ One-way inactive → archived ✅ One-way archived → active ❌ Not allowed archived → inactive ❌ Not allowed ``` ### Transition API ```bash # Active → Inactive curl -X PUT https://api-sandbox.fndev.net/api/v1/webhooks/wh_abc123 \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{"status": "inactive"}' # Inactive → Active curl -X PUT https://api-sandbox.fndev.net/api/v1/webhooks/wh_abc123 \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{"status": "active"}' # Any → Archived (permanent) curl -X PUT https://api-sandbox.fndev.net/api/v1/webhooks/wh_abc123 \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{"status": "archived"}' ``` --- ## Automatic State Changes Field Nation may automatically change webhook state under certain conditions: ### Auto-Deactivation **Trigger**: 7 consecutive days of failed deliveries **Action**: `active` → `inactive` **Notification**: Email sent to `notificationEmail` (if configured) **Reason**: Prevent infinite retry loops on broken endpoints **Recovery**: ```bash # Fix endpoint issues first, then reactivate curl -X PUT https://api-sandbox.fndev.net/api/v1/webhooks/wh_abc123 \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{"status": "active"}' ``` > **Important**: Fix underlying issues before reactivating. Repeated auto-deactivations may result in webhook being permanently archived. --- ## Change History & Audit Trail Every webhook modification is tracked in a comprehensive change history. ### Tracked Changes - Status transitions (active ⟷ inactive, → archived) - URL modifications - Event subscriptions (added/removed) - HTTP method changes - Secret regeneration - Custom header updates - Notification email changes ### Accessing Change History #### Via API ```bash curl -X GET "https://api-sandbox.fndev.net/api/v1/webhooks/wh_abc123/history" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` **Response:** ```json { "metadata": { "timestamp": "2026-01-15T11:00:00Z", "count": 5, "total": 5 }, "result": [ { "id": 123, "userId": 456, "action": "status_changed", "changes": { "status": { "from": "active", "to": "inactive" } }, "createdAt": "2026-01-15T10:45:00Z", "updatedAt": "2026-01-15T10:45:00Z" }, { "id": 122, "userId": 456, "action": "events_updated", "changes": { "events": { "added": ["workorder.status.approved"], "removed": ["workorder.status.draft"] } }, "createdAt": "2026-01-15T09:30:00Z", "updatedAt": "2026-01-15T09:30:00Z" }, { "id": 121, "userId": 456, "action": "url_changed", "changes": { "url": { "from": "https://old-endpoint.com/webhook", "to": "https://new-endpoint.com/webhooks" } }, "createdAt": "2026-01-14T15:20:00Z", "updatedAt": "2026-01-14T15:20:00Z" } ] } ``` #### Via Web UI 1. Navigate to [Webhooks Dashboard](https://ui-sandbox.fndev.net/integrations/webhooks) 2. Click on webhook 3. Go to "History" tab 4. View chronological list of changes ![Webhook Change History](../images/webhook-update-history.webp) ### Filter History ```bash # Filter by user curl -X GET "https://api-sandbox.fndev.net/api/v1/webhooks/wh_abc123/history?userId=456" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" # Filter by action curl -X GET "https://api-sandbox.fndev.net/api/v1/webhooks/wh_abc123/history?search=status_changed" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` --- ## Lifecycle Management Best Practices ### Use Descriptive Notification Emails Configure an email to receive alerts: ```json { "notificationEmail": "webhook-alerts+production@example.com" } ``` Benefits: - Receive auto-deactivation warnings - Track delivery failures - Monitor webhook health ### Tag Webhooks for Organization Use naming conventions or custom headers to identify webhooks: ```javascript // Create webhook with identifier in URL or custom headers { "url": "https://your-endpoint.com/webhooks/production", "webhookAttribute": { "header": { "X-Webhook-Environment": "production", "X-Webhook-Owner": "team-integrations" } } } ``` ### Monitor State Changes Track webhook state changes in your monitoring system: ```javascript async function checkWebhookHealth() { const webhooks = await fetch( 'https://api-sandbox.fndev.net/api/v1/webhooks', { headers: { 'Authorization': `Bearer ${accessToken}` } } ).then(r => r.json()); const inactive = webhooks.result.filter(w => w.status === 'inactive'); if (inactive.length > 0) { await alertTeam(`${inactive.length} webhooks are inactive`, inactive); } } ``` ### Implement Graceful Deprecation When replacing webhooks: ### Create New Webhook Create the new webhook with updated configuration: ```bash 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://new-endpoint.com/webhooks", "method": "post", "status": "active", "events": ["workorder.status.published"] }' ``` ### Test New Webhook Verify new webhook works correctly before deactivating old one. ### Pause Old Webhook Set old webhook to `inactive`: ```bash curl -X PUT https://api-sandbox.fndev.net/api/v1/webhooks/wh_old123 \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{"status": "inactive"}' ``` ### Monitor for Issues Watch for missing events or processing errors. ### Archive Old Webhook Once confident, archive the old webhook: ```bash curl -X PUT https://api-sandbox.fndev.net/api/v1/webhooks/wh_old123 \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{"status": "archived"}' ``` --- ## Viewing Webhook Status ### List Webhooks by Status ```bash curl -X GET "https://api-sandbox.fndev.net/api/v1/webhooks?status=active" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ```bash curl -X GET "https://api-sandbox.fndev.net/api/v1/webhooks?status=inactive" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ```bash curl -X GET "https://api-sandbox.fndev.net/api/v1/webhooks?status=archived" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ```bash curl -X GET "https://api-sandbox.fndev.net/api/v1/webhooks" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ### Get Specific Webhook Status ```bash curl -X GET https://api-sandbox.fndev.net/api/v1/webhooks/wh_abc123 \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` **Response:** ```json { "metadata": { "timestamp": "2026-01-15T11:00:00Z" }, "result": { "id": 123, "webhookId": "wh_abc123", "url": "https://your-endpoint.com/webhooks", "method": "post", "status": "active", "events": ["workorder.status.published", "workorder.status.assigned"], "createdAt": "2026-01-10T10:00:00Z", "updatedAt": "2026-01-15T10:45:00Z" } } ``` --- ## Lifecycle Event Flow ```mermaid graph TD A[Create Webhook] -->|Default| B[Active] A -->|Specify inactive| C[Inactive] B -->|Process Events| D[Event Occurs] D -->|Queue & Deliver| E[Delivery Attempt] E -->|Success| F[Log Success] E -->|Failure| G{7 Days Failed?} G -->|Yes| H[Auto-Deactivate to Inactive] G -->|No| I[Retry with Backoff] B -->|Manual Pause| C C -->|Manual Reactivate| B B -->|Permanent Deactivate| J[Archived] C -->|Permanent Deactivate| J J -->|Read-Only| K[Historical Record] ``` --- --- ### Creating webhooks URL: /docs/webhooks/guides/creating-webhooks ## Prerequisites Before creating a webhook, ensure you have: - ✅ Field Nation API credentials (`client_id` and `client_secret`) - ✅ OAuth access token (for API method) - ✅ HTTPS endpoint URL ready to receive webhooks - ✅ List of events you want to subscribe to [Review prerequisites →](/docs/getting-started/prerequisites) --- ## Method 1: Web UI (Visual) Best for: - Quick setup and testing - Non-technical users - Visual configuration - One-off webhook creation ### Navigate to Webhooks Dashboard **Sandbox**: [https://ui-sandbox.fndev.net/integrations/webhooks](https://ui-sandbox.fndev.net/integrations/webhooks) **Production**: [https://app.fieldnation.com/integrations/webhooks](https://app.fieldnation.com/integrations/webhooks) ### Click "Create New" Located in the top-right corner of the dashboard. ### Configure Basic Settings Fill in the required fields: **Webhook URL**: - Enter your HTTPS endpoint (e.g., `https://your-endpoint.com/webhooks`) - Must be publicly accessible - Must use HTTPS (HTTP not allowed) **HTTP Method**: - **POST** (recommended) - Standard for webhooks - **PUT** - Use if your endpoint requires PUT **Status**: - **Active** - Start receiving events immediately - **Inactive** - Create paused, activate later ### Select Events Choose which events trigger this webhook: **Option 1: Select All Events** - Check "Subscribe to all events" - Receives all 33 events automatically - Future events included automatically **Option 2: Select Specific Events** - Click "Add Event" - Search or browse available events - Select only events your system processes > [INFO] **Recommendation**: Start with a small set of events (4-6), then expand as needed. Subscribing to unused events creates unnecessary processing overhead. ### Configure Advanced Settings (Optional) Click "Advanced Settings" to configure: **Notification Email**: - Email address for delivery failure alerts - Recommended: Use a monitored team email **Custom Headers**: - Add authentication tokens or custom identifiers - Click "Add Header" - Enter key-value pairs **Example:** ```plaintext Authorization: Bearer your-api-token X-Custom-ID: integration-prod X-Environment: production ``` > **Reserved Prefix**: Headers cannot start with `x-fn-` (reserved for Field Nation). **Secret Key**: - Auto-generated by default - Optionally provide your own UUID - Used for HMAC-SHA256 signature verification ### Review & Create - Review all settings - Click "Create Webhook" - **Save the webhook ID and secret** - you'll need these ### UI Configuration Example ![Webhook Creation Form](../images/create-webhook.webp) **Advanced Settings:** ![Advanced Webhook Configuration](../images/create-webhook-advanced.webp) > [INFO] **After Creation**: The webhook is immediately active if status was set to "active". Test by triggering an event! --- ## Method 2: REST API (Programmatic) Best for: - Automation and infrastructure-as-code - Multiple webhook creation - CI/CD pipelines - Dynamic configuration ### Step 1: Get OAuth Access Token ```bash title="Generate Access Token" curl -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:** ```json { "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", "token_type": "Bearer", "expires_in": 3600 } ``` ### Step 2: Create Webhook ```bash title="Create Webhook" 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", "workorder.status.work_done", "workorder.status.approved" ], "notificationEmail": "webhook-alerts@example.com" }' ``` **Response:** ```json { "metadata": { "timestamp": "2026-01-15T10:30:00Z" }, "result": { "id": 123, "webhookId": "wh_abc123def456", "companyId": 789, "userId": 456, "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", "workorder.status.work_done", "workorder.status.approved" ], "notificationEmail": "webhook-alerts@example.com", "modelProperties": [], "isIntegrationOnly": false, "createdAt": "2026-01-15T10:30:00Z", "updatedAt": "2026-01-15T10:30:00Z" } } ``` > **Save the secret!** The `secret` field is only returned during creation. Store it securely—you'll need it for signature verification. ### Step 3: Verify Webhook Created ```bash title="Get Webhook Details" curl -X GET https://api-sandbox.fndev.net/api/v1/webhooks/wh_abc123def456 \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` --- ## Request Body Reference ### Required Fields ", description: "Array of event names to subscribe to. Must contain at least 1 event. See [event catalog](/docs/webhooks/concepts/events) for complete list.", required: true } }} /> ### Optional Fields --- ## Configuration Examples ### Minimal Configuration Subscribe to single event: ```json { "url": "https://your-endpoint.com/webhooks", "method": "post", "status": "active", "events": ["workorder.status.published"] } ``` ### Production-Ready Configuration Full configuration with custom headers and notifications: ```json { "url": "https://api.example.com/integrations/fieldnation/webhooks", "method": "post", "status": "active", "events": [ "workorder.created", "workorder.status.published", "workorder.status.assigned", "workorder.status.checked_in", "workorder.status.work_done", "workorder.status.approved", "workorder.status.paid" ], "secret": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "notificationEmail": "integrations-alerts@example.com", "webhookAttribute": { "header": { "Authorization": "Bearer prod-api-token-xyz", "X-Webhook-Environment": "production", "X-Webhook-Version": "v1", "X-Team-Owner": "integrations" } } } ``` ### Multi-Environment Setup Create separate webhooks for different environments: ```json { "url": "https://dev.example.com/webhooks", "method": "post", "status": "active", "events": ["workorder.created", "workorder.status.published"], "notificationEmail": "dev-team@example.com", "webhookAttribute": { "header": { "X-Environment": "development" } } } ``` ```json { "url": "https://staging.example.com/webhooks", "method": "post", "status": "active", "events": [ "workorder.created", "workorder.status.published", "workorder.status.assigned", "workorder.status.work_done" ], "notificationEmail": "staging-alerts@example.com", "webhookAttribute": { "header": { "X-Environment": "staging" } } } ``` ```json { "url": "https://api.example.com/webhooks", "method": "post", "status": "active", "events": [ "workorder.created", "workorder.status.published", "workorder.status.assigned", "workorder.status.checked_in", "workorder.status.work_done", "workorder.status.approved", "workorder.status.paid", "workorder.problem_reported" ], "notificationEmail": "prod-alerts@example.com", "webhookAttribute": { "header": { "Authorization": "Bearer prod-api-token", "X-Environment": "production" } } } ``` --- ## Adding Custom Headers Custom headers are sent with every webhook delivery: ### Authentication Headers ```json { "webhookAttribute": { "header": { "Authorization": "Bearer your-api-token", "X-API-Key": "your-api-key" } } } ``` ### Identifier Headers ```json { "webhookAttribute": { "header": { "X-Webhook-ID": "prod-wh-001", "X-Environment": "production", "X-Version": "v1", "X-Owner": "integrations-team" } } } ``` > [INFO] **Best Practice**: Use custom headers for authentication instead of query parameters. Headers are encrypted in transit and stored encrypted in the database. --- ## Event Selection Strategies ### Strategy 1: Minimal (Core Events) Subscribe only to critical lifecycle events: ```json { "events": [ "workorder.status.published", "workorder.status.assigned", "workorder.status.work_done", "workorder.status.approved" ] } ``` **Use When**: Building MVP or simple integration **Benefits**: Minimal processing, easy to test, focused integration --- ### Strategy 2: Comprehensive (All Status Changes) Subscribe to complete status lifecycle: ```json { "events": [ "workorder.status.published", "workorder.status.confirmed", "workorder.status.assigned", "workorder.status.on_my_way", "workorder.status.checked_in", "workorder.status.checked_out", "workorder.status.work_done", "workorder.status.approved", "workorder.status.paid", "workorder.status.cancelled" ] } ``` **Use When**: Need complete visibility into work order progress **Benefits**: Real-time dashboards, detailed tracking, comprehensive sync --- ### Strategy 3: Activity-Focused Subscribe to work order activities: ```json { "events": [ "workorder.message_posted", "workorder.provider_upload", "workorder.task_completed", "workorder.schedule_updated", "workorder.problem_reported", "workorder.problem_resolved" ] } ``` **Use When**: Monitoring provider activity and work progress **Benefits**: Track communications, document uploads, task completion --- ### Strategy 4: All Events Subscribe to everything: ```json { "events": [ "workorder.created", "workorder.routed", // ... all 33 events ] } ``` **Use When**: Building comprehensive integration or analytics **Caution**: High event volume, requires robust processing --- ## Infrastructure as Code ### Terraform Example ```hcl resource "fieldnation_webhook" "production" { url = "https://api.example.com/webhooks" method = "post" status = "active" events = [ "workorder.status.published", "workorder.status.assigned", "workorder.status.work_done", "workorder.status.approved" ] notification_email = "prod-alerts@example.com" custom_headers = { "Authorization" = "Bearer ${var.api_token}" "X-Environment" = "production" } } ``` ### Script-Based Creation ```javascript title="create-webhook.js" const fetch = require('node-fetch'); async function createWebhook(config) { // Get access token const tokenResponse = await fetch( 'https://api-sandbox.fndev.net/authentication/api/oauth/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'client_credentials', client_id: process.env.FN_CLIENT_ID, client_secret: process.env.FN_CLIENT_SECRET }) } ); const { access_token } = await tokenResponse.json(); // Create webhook const webhookResponse = await fetch( 'https://api-sandbox.fndev.net/api/v1/webhooks', { method: 'POST', headers: { 'Authorization': `Bearer ${access_token}`, 'Content-Type': 'application/json' }, body: JSON.stringify(config) } ); const webhook = await webhookResponse.json(); console.log('Webhook created:', webhook.result.webhookId); console.log('Secret:', webhook.result.secret); return webhook.result; } // Usage createWebhook({ url: 'https://your-endpoint.com/webhooks', method: 'post', status: 'active', events: ['workorder.status.published', 'workorder.status.assigned'], notificationEmail: 'alerts@example.com' }); ``` --- ## Best Practices ### Start Inactive for Testing Create webhooks in `inactive` state for safe testing: ```json { "status": "inactive", // ... other config } ``` Activate after verifying endpoint: ```bash curl -X PUT https://api-sandbox.fndev.net/api/v1/webhooks/wh_abc123 \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{"status": "active"}' ``` ### Use Descriptive Notification Emails ```json { "notificationEmail": "webhook-prod-team-integrations@example.com" } ``` Benefits: - Easy to identify which webhook failed - Route to correct team - Track multiple environments ### Generate Strong Secrets ```javascript const crypto = require('crypto'); const uuid = require('uuid'); // Option 1: UUID v4 const secret = uuid.v4(); // Option 2: Random hex const secret = crypto.randomBytes(32).toString('hex'); ``` ### Document Your Webhooks Maintain a registry: ```markdown | Webhook ID | Environment | Events | Owner | Purpose | |------------|-------------|--------|-------|---------| | wh_prod_01 | Production | Status changes | Integrations | Salesforce sync | | wh_prod_02 | Production | All events | Analytics | Data warehouse | | wh_dev_01 | Development | Core events | Dev Team | Local testing | ``` --- --- ### Handling events URL: /docs/webhooks/guides/handling-events ## Event Processing Architecture ```mermaid graph TD A[Webhook Received] --> B{Verify Signature} B -->|Invalid| C[Reject 401] B -->|Valid| D{Check Idempotency} D -->|Duplicate| E[Return 200 Already Processed] D -->|New| F[Respond 200 OK] F --> G[Queue for Async Processing] G --> H[Process Event] H --> I{Success?} I -->|Yes| J[Mark Complete] I -->|No| K{Retriable?} K -->|Yes| L[Requeue with Backoff] K -->|No| M[Send to DLQ] L --> H ``` --- ## 1. Idempotency **Critical**: Webhooks may be delivered more than once. Your system must handle duplicate events gracefully. ### Why Idempotency Matters - **Retry logic**: Failed deliveries are retried - **Network issues**: Timeout may occur after processing - **Manual retries**: Operators can manually retry events - **At-least-once delivery**: Field Nation guarantees at-least-once, not exactly-once ### Implementing Idempotency Use `eventId` as the unique identifier: ```javascript const redis = require('redis'); const client = redis.createClient(); async function isEventProcessed(eventId) { return await client.exists(`event:${eventId}`); } async function markEventProcessed(eventId) { // Store for 7 days (604800 seconds) await client.setex(`event:${eventId}`, 604800, 'processed'); } async function processWebhook(payload) { const { eventId } = payload; // Check if already processed if (await isEventProcessed(eventId)) { console.log(`Duplicate event ${eventId}, skipping`); return { status: 'duplicate', eventId }; } // Mark as processing (prevents race conditions) const wasSet = await client.set( `event:${eventId}`, 'processing', 'EX', 604800, 'NX' // Only set if not exists ); if (!wasSet) { console.log(`Event ${eventId} already being processed`); return { status: 'duplicate', eventId }; } try { // Process the event await handleEvent(payload); // Update to processed await client.set(`event:${eventId}`, 'processed', 'EX', 604800); return { status: 'processed', eventId }; } catch (error) { // Remove processing lock on failure await client.del(`event:${eventId}`); throw error; } } ``` ```sql -- Create processed_events table CREATE TABLE processed_events ( event_id VARCHAR(255) PRIMARY KEY, event_name VARCHAR(255) NOT NULL, work_order_id INTEGER, processed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, payload JSONB, status VARCHAR(50) DEFAULT 'processed' ); -- Index for cleanup CREATE INDEX idx_processed_at ON processed_events(processed_at); ``` ```javascript const { Pool } = require('pg'); const pool = new Pool(); async function isEventProcessed(eventId) { const result = await pool.query( 'SELECT event_id FROM processed_events WHERE event_id = $1', [eventId] ); return result.rows.length > 0; } async function markEventProcessed(eventId, eventName, workOrderId, payload) { try { await pool.query( `INSERT INTO processed_events (event_id, event_name, work_order_id, payload, status) VALUES ($1, $2, $3, $4, 'processed') ON CONFLICT (event_id) DO NOTHING`, [eventId, eventName, workOrderId, JSON.stringify(payload)] ); } catch (error) { // Unique constraint violation means already processed if (error.code === '23505') { return false; } throw error; } return true; } async function processWebhook(payload) { const { eventId, eventName, workOrderId } = payload; // Try to insert event (atomic operation) const wasInserted = await markEventProcessed( eventId, eventName, workOrderId, { status: 'processing' } ); if (!wasInserted) { console.log(`Duplicate event ${eventId}`); return { status: 'duplicate', eventId }; } try { // Process the event await handleEvent(payload); // Update status await pool.query( 'UPDATE processed_events SET status = $1, payload = $2 WHERE event_id = $3', ['processed', JSON.stringify(payload), eventId] ); return { status: 'processed', eventId }; } catch (error) { // Mark as failed await pool.query( 'UPDATE processed_events SET status = $1 WHERE event_id = $2', ['failed', eventId] ); throw error; } } // Cleanup old events (run daily) async function cleanupOldEvents() { await pool.query( 'DELETE FROM processed_events WHERE processed_at < NOW() - INTERVAL \'7 days\'' ); } ``` ```javascript // WARNING: Only for development - data lost on restart const processedEvents = new Set(); function isEventProcessed(eventId) { return processedEvents.has(eventId); } function markEventProcessed(eventId) { processedEvents.add(eventId); } async function processWebhook(payload) { const { eventId } = payload; if (isEventProcessed(eventId)) { console.log(`Duplicate event ${eventId}`); return { status: 'duplicate', eventId }; } markEventProcessed(eventId); try { await handleEvent(payload); return { status: 'processed', eventId }; } catch (error) { // Remove on failure to allow retry processedEvents.delete(eventId); throw error; } } ``` > **Important**: Store idempotency keys for at least 7 days to handle late retries and manual replays. --- ## 2. Async Processing Pattern **Critical**: Respond to webhooks within 5 seconds, then process asynchronously. ### Why Async Processing? - **Fast response**: Prevents timeouts and retries - **Scalability**: Handle high event volumes - **Reliability**: Failures don't block webhook delivery - **Resource management**: Control processing concurrency ### Basic Async Pattern ```javascript const express = require('express'); const { Queue } = require('bull'); const app = express(); const webhookQueue = new Queue('webhooks', 'redis://localhost:6379'); // Webhook endpoint - respond immediately app.post('/webhooks/fieldnation', async (req, res) => { try { // 1. Verify signature if (!verifySignature(req.body, req.headers['x-fn-signature'])) { return res.status(401).send('Unauthorized'); } // 2. Parse payload const payload = JSON.parse(req.body.toString()); // 3. Check idempotency if (await isEventProcessed(payload.eventId)) { console.log(`Duplicate: ${payload.eventId}`); return res.status(200).send('Already processed'); } // 4. Queue for processing await webhookQueue.add('process', payload, { attempts: 5, backoff: { type: 'exponential', delay: 2000 }, removeOnComplete: true, removeOnFail: false }); // 5. Respond immediately res.status(200).send('OK'); } catch (error) { console.error('Webhook error:', error); res.status(500).send('Internal error'); } }); // Worker - process async webhookQueue.process('process', async (job) => { const payload = job.data; try { // Mark as processing await markEventProcessing(payload.eventId); // Process the event await processEvent(payload); // Mark as complete await markEventProcessed(payload.eventId); return { status: 'success', eventId: payload.eventId }; } catch (error) { console.error(`Failed to process ${payload.eventId}:`, error); throw error; // Will trigger retry } }); async function processEvent(payload) { const { eventName, data } = payload; switch (eventName) { case 'workorder.status.published': await handlePublished(data); break; case 'workorder.status.assigned': await handleAssigned(data); break; case 'workorder.status.work_done': await handleWorkDone(data); break; default: console.log(`Unhandled event: ${eventName}`); } } ``` ### Advanced: Multi-Queue Architecture Separate queues by priority or event type: ```javascript const highPriorityQueue = new Queue('webhooks-high', 'redis://localhost:6379'); const normalQueue = new Queue('webhooks-normal', 'redis://localhost:6379'); const lowPriorityQueue = new Queue('webhooks-low', 'redis://localhost:6379'); function getQueueForEvent(eventName) { // High priority: payment, approval if (eventName.includes('approved') || eventName.includes('paid')) { return highPriorityQueue; } // Low priority: messages, uploads if (eventName.includes('message') || eventName.includes('upload')) { return lowPriorityQueue; } // Normal priority: everything else return normalQueue; } app.post('/webhooks/fieldnation', async (req, res) => { const payload = JSON.parse(req.body.toString()); const queue = getQueueForEvent(payload.eventName); await queue.add('process', payload); res.status(200).send('OK'); }); // Process each queue with different concurrency highPriorityQueue.process('process', 10, processEvent); normalQueue.process('process', 5, processEvent); lowPriorityQueue.process('process', 2, processEvent); ``` --- ## 3. Error Handling Robust error handling prevents data loss and ensures reliability. ### Error Categories **Retry these errors** - temporary issues that may resolve: ```javascript const RETRIABLE_ERRORS = [ 'ECONNREFUSED', // Connection refused 'ETIMEDOUT', // Connection timeout 'ENOTFOUND', // DNS lookup failed 'ENETUNREACH', // Network unreachable 'ECONNRESET', // Connection reset '503', // Service Unavailable '504', // Gateway Timeout '429' // Too Many Requests ]; function isRetriable(error) { return RETRIABLE_ERRORS.some(code => error.code === code || error.message.includes(code) || error.statusCode === parseInt(code) ); } async function processWithRetry(payload) { try { await syncToSalesforce(payload.data); } catch (error) { if (isRetriable(error)) { console.log(`Retriable error for ${payload.eventId}:`, error.message); throw error; // Bull will retry } // Non-retriable error - log and move to DLQ console.error(`Non-retriable error for ${payload.eventId}:`, error); await sendToDLQ(payload, error); } } ``` **Don't retry these** - permanent failures: ```javascript const NON_RETRIABLE_ERRORS = [ 'VALIDATION_ERROR', // Invalid data format 'AUTHENTICATION_ERROR', // Invalid credentials 'AUTHORIZATION_ERROR', // Insufficient permissions 'NOT_FOUND', // Resource doesn't exist '400', // Bad Request '401', // Unauthorized '403', // Forbidden '404', // Not Found '422' // Unprocessable Entity ]; async function processEvent(payload) { try { // Validate payload if (!validatePayload(payload)) { throw new Error('VALIDATION_ERROR: Invalid payload format'); } // Process event await handleEvent(payload); } catch (error) { if (isNonRetriable(error)) { // Log and move to DLQ immediately console.error(`Non-retriable error for ${payload.eventId}:`, error); await sendToDLQ(payload, error); return; // Don't throw - prevents retry } throw error; // Retriable - let it retry } } ``` **Handle partial success** - some operations succeed: ```javascript async function processEvent(payload) { const results = { salesforce: null, database: null, notification: null }; try { // Step 1: Update Salesforce results.salesforce = await syncToSalesforce(payload.data); } catch (error) { console.error('Salesforce sync failed:', error); // Continue - don't fail entire job } try { // Step 2: Update database results.database = await updateDatabase(payload.data); } catch (error) { console.error('Database update failed:', error); throw error; // Critical failure - retry everything } try { // Step 3: Send notification results.notification = await sendNotification(payload.data); } catch (error) { console.error('Notification failed:', error); // Log but don't fail - not critical } // Log results await logProcessingResults(payload.eventId, results); return results; } ``` ### Dead Letter Queue (DLQ) Send permanently failed events to DLQ for manual review: ```javascript const dlqQueue = new Queue('webhooks-dlq', 'redis://localhost:6379'); async function sendToDLQ(payload, error) { await dlqQueue.add('failed', { payload, error: { message: error.message, stack: error.stack, code: error.code }, failedAt: new Date().toISOString(), attempts: payload.attempts || 0 }, { removeOnComplete: false, // Keep DLQ items removeOnFail: false }); // Alert team await alertTeam({ type: 'webhook_dlq', eventId: payload.eventId, eventName: payload.eventName, error: error.message }); } // Monitor DLQ dlqQueue.on('completed', async (job) => { console.log(`DLQ item resolved: ${job.data.payload.eventId}`); }); ``` --- ## 4. Event-Specific Handlers Organize your code by event type for clarity and maintainability: ```javascript // handlers/workorder.js class WorkOrderHandlers { async handlePublished(data) { console.log(`Work order ${data.id} published`); // Notify provider network await this.notifyProviders(data); // Update dispatch board await this.updateDispatchBoard(data); // Sync to Salesforce await this.syncToSalesforce(data); } async handleAssigned(data) { console.log(`Work order ${data.id} assigned to provider ${data.provider.id}`); // Notify provider await this.notifyProvider(data.provider, data); // Update scheduling system await this.updateSchedule(data); // Log assignment await this.logAssignment(data); } async handleWorkDone(data) { console.log(`Work order ${data.id} work completed`); // Trigger approval workflow await this.triggerApprovalWorkflow(data); // Notify buyer await this.notifyBuyer(data.buyer, data); // Update analytics await this.updateMetrics(data); } async handleApproved(data) { console.log(`Work order ${data.id} approved`); // Generate invoice await this.generateInvoice(data); // Update accounting system await this.updateAccounting(data); // Archive work order await this.archiveWorkOrder(data); } } // handlers/index.js const workOrderHandlers = new WorkOrderHandlers(); async function processEvent(payload) { const { eventName, data } = payload; // Route to appropriate handler switch (eventName) { case 'workorder.status.published': return await workOrderHandlers.handlePublished(data); case 'workorder.status.assigned': return await workOrderHandlers.handleAssigned(data); case 'workorder.status.work_done': return await workOrderHandlers.handleWorkDone(data); case 'workorder.status.approved': return await workOrderHandlers.handleApproved(data); default: console.log(`No handler for event: ${eventName}`); } } ``` --- ## 5. Circuit Breaker Pattern Prevent cascading failures when downstream services are unavailable: ```javascript const CircuitBreaker = require('opossum'); // Configure circuit breaker const salesforceBreaker = new CircuitBreaker(async (data) => { return await syncToSalesforce(data); }, { timeout: 5000, // 5 second timeout errorThresholdPercentage: 50, // Open after 50% failures resetTimeout: 30000, // Try again after 30 seconds rollingCountTimeout: 10000, // Track errors over 10 seconds rollingCountBuckets: 10, // 10 buckets name: 'salesforce' }); // Monitor circuit breaker state salesforceBreaker.on('open', () => { console.error('Circuit breaker OPEN - Salesforce unavailable'); alertTeam({ service: 'salesforce', status: 'circuit_open' }); }); salesforceBreaker.on('halfOpen', () => { console.log('Circuit breaker HALF-OPEN - Testing Salesforce'); }); salesforceBreaker.on('close', () => { console.log('Circuit breaker CLOSED - Salesforce recovered'); }); // Use in event handler async function handlePublished(data) { try { await salesforceBreaker.fire(data); } catch (error) { if (salesforceBreaker.opened) { console.log('Salesforce circuit open, queuing for later'); await queueForLater(data); } else { throw error; } } } ``` --- ## 6. Rate Limiting Downstream Services Protect your downstream services from overload: ```javascript const Bottleneck = require('bottleneck'); // Rate limiter: max 10 requests per second const salesforceLimiter = new Bottleneck({ maxConcurrent: 5, // Max 5 concurrent requests minTime: 100, // Min 100ms between requests reservoir: 10, // 10 requests reservoirRefreshAmount: 10, reservoirRefreshInterval: 1000 // per second }); async function syncToSalesforce(data) { return await salesforceLimiter.schedule(async () => { const response = await fetch('https://salesforce.com/api/workorders', { method: 'POST', headers: { 'Authorization': `Bearer ${token}` }, body: JSON.stringify(data) }); return await response.json(); }); } ``` --- ## 7. Monitoring & Observability Track webhook processing health: ```javascript const prometheus = require('prom-client'); // Metrics const webhookCounter = new prometheus.Counter({ name: 'webhooks_received_total', help: 'Total webhooks received', labelNames: ['event_name', 'status'] }); const webhookDuration = new prometheus.Histogram({ name: 'webhook_processing_duration_seconds', help: 'Time to process webhook', labelNames: ['event_name'], buckets: [0.1, 0.5, 1, 2, 5, 10] }); const webhookErrors = new prometheus.Counter({ name: 'webhook_errors_total', help: 'Total webhook processing errors', labelNames: ['event_name', 'error_type'] }); // Instrumented handler async function processEvent(payload) { const timer = webhookDuration.startTimer({ event_name: payload.eventName }); try { await handleEvent(payload); webhookCounter.inc({ event_name: payload.eventName, status: 'success' }); timer(); } catch (error) { webhookCounter.inc({ event_name: payload.eventName, status: 'error' }); webhookErrors.inc({ event_name: payload.eventName, error_type: error.code || 'unknown' }); timer(); throw error; } } ``` --- ## Complete Production Example Here's a full production-ready webhook handler: ```javascript title="webhook-handler.js" const express = require('express'); const { Queue } = require('bull'); const CircuitBreaker = require('opossum'); const redis = require('redis'); const app = express(); const redisClient = redis.createClient(); const webhookQueue = new Queue('webhooks', 'redis://localhost:6379'); // Circuit breakers const salesforceBreaker = new CircuitBreaker(syncToSalesforce, { timeout: 5000, errorThresholdPercentage: 50, resetTimeout: 30000 }); // Webhook endpoint app.use(express.raw({ type: 'application/json' })); app.post('/webhooks/fieldnation', async (req, res) => { try { // 1. Verify signature if (!verifySignature(req.body, req.headers['x-fn-signature'], process.env.WEBHOOK_SECRET)) { return res.status(401).send('Unauthorized'); } // 2. Parse payload const payload = JSON.parse(req.body.toString()); // 3. Check idempotency const alreadyProcessed = await redisClient.exists(`event:${payload.eventId}`); if (alreadyProcessed) { return res.status(200).send('Already processed'); } // 4. Queue for async processing await webhookQueue.add('process', payload, { attempts: 5, backoff: { type: 'exponential', delay: 2000 }, removeOnComplete: true }); // 5. Respond immediately res.status(200).send('OK'); } catch (error) { console.error('Webhook endpoint error:', error); res.status(500).send('Internal error'); } }); // Worker webhookQueue.process('process', 5, async (job) => { const payload = job.data; try { // Mark as processing await redisClient.set(`event:${payload.eventId}`, 'processing', 'EX', 604800, 'NX'); // Process event await processEvent(payload); // Mark complete await redisClient.set(`event:${payload.eventId}`, 'processed', 'EX', 604800); } catch (error) { console.error(`Processing failed for ${payload.eventId}:`, error); if (isNonRetriable(error)) { await sendToDLQ(payload, error); return; // Don't throw - prevents retry } throw error; // Retriable } }); async function processEvent(payload) { const { eventName, data } = payload; switch (eventName) { case 'workorder.status.published': await salesforceBreaker.fire(data); break; case 'workorder.status.assigned': await handleAssigned(data); break; case 'workorder.status.work_done': await handleWorkDone(data); break; default: console.log(`Unhandled event: ${eventName}`); } } app.listen(3000); ``` --- ## Best Practices Checklist - ✅ Implement idempotency with `eventId` - ✅ Respond to webhooks within 5 seconds - ✅ Process events asynchronously - ✅ Handle retriable vs non-retriable errors differently - ✅ Use circuit breakers for downstream services - ✅ Rate limit calls to external APIs - ✅ Implement Dead Letter Queue for failed events - ✅ Monitor processing metrics - ✅ Log errors with context - ✅ Set up alerts for anomalies --- --- ### Monitoring URL: /docs/webhooks/guides/monitoring ## Monitoring Strategy ```mermaid graph LR A[Webhook Delivery] --> B[Metrics Collection] B --> C[Dashboards] B --> D[Alerts] B --> E[Logs] C --> F[Real-time Monitoring] D --> G[Incident Response] E --> H[Debugging] F --> I[Proactive Management] G --> I H --> I ``` --- ## Key Metrics to Track ### 1. Delivery Metrics 99%", required: true }, "delivery_latency": { type: "duration (ms)", description: "Time from event trigger to successful delivery. Target: ### 2. Processing Metrics ### 3. Business Metrics --- ## Monitoring Field Nation's Delivery Logs Access comprehensive delivery data via Field Nation's API: ### List Delivery Logs ```bash curl -X GET "https://api-sandbox.fndev.net/api/v1/webhooks/delivery-logs?webhookId=wh_abc123&page=1&perPage=50" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` **Response:** ```json { "metadata": { "timestamp": "2026-01-15T12:00:00Z", "count": 50, "total": 1234 }, "result": [ { "deliveryId": "del_xyz789", "webhookId": "wh_abc123", "workOrderId": 12345, "eventName": "workorder.status.published", "deliveryStatus": 200, "deliveryAttempt": 1, "createdAt": "2026-01-15T11:59:00Z" } // ... more deliveries ] } ``` ### Filter by Status ```bash # Failed deliveries only curl -X GET "https://api-sandbox.fndev.net/api/v1/webhooks/delivery-logs?webhookId=wh_abc123&deliveryStatus=500" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" # Retried deliveries curl -X GET "https://api-sandbox.fndev.net/api/v1/webhooks/delivery-logs?webhookId=wh_abc123&sortBy=deliveryAttempt&sortDirection=DESC" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ### Get Detailed Log ```bash curl -X GET https://api-sandbox.fndev.net/api/v1/webhooks/delivery-logs/del_xyz789 \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` **Response includes pre-signed URL to full log file:** ```json { "metadata": { "timestamp": "2026-01-15T12:00:00Z" }, "result": { "deliveryId": "del_xyz789", "webhookId": "wh_abc123", "eventName": "workorder.status.published", "deliveryStatus": 200, "deliveryAttempt": 1, "createdAt": "2026-01-15T11:59:00Z", "delivery_log": "https://s3.amazonaws.com/fn-logs/del_xyz789?expires=..." } } ``` ![Delivery Logs Dashboard](../images/delivery-logs.webp) --- ## Automated Monitoring Script Poll delivery logs and alert on issues: ```javascript title="monitor-webhooks.js" const fetch = require('node-fetch'); class WebhookMonitor { constructor(accessToken) { this.accessToken = accessToken; this.baseUrl = 'https://api-sandbox.fndev.net'; } async getDeliveryLogs(webhookId, filters = {}) { const params = new URLSearchParams({ webhookId, perPage: 100, ...filters }); const response = await fetch( `${this.baseUrl}/api/v1/webhooks/delivery-logs?${params}`, { headers: { 'Authorization': `Bearer ${this.accessToken}` } } ); return await response.json(); } async calculateSuccessRate(webhookId, minutes = 60) { const sinceTime = new Date(Date.now() - minutes * 60 * 1000).toISOString(); const logs = await this.getDeliveryLogs(webhookId, { sortBy: 'createdAt', sortDirection: 'DESC' }); // Filter to time window const recentLogs = logs.result.filter(log => new Date(log.createdAt) >= new Date(sinceTime) ); if (recentLogs.length === 0) return 100; const successful = recentLogs.filter(log => log.deliveryStatus >= 200 && log.deliveryStatus < 300 ).length; return (successful / recentLogs.length) * 100; } async getRetryRate(webhookId, minutes = 60) { const sinceTime = new Date(Date.now() - minutes * 60 * 1000).toISOString(); const logs = await this.getDeliveryLogs(webhookId); const recentLogs = logs.result.filter(log => new Date(log.createdAt) >= new Date(sinceTime) ); if (recentLogs.length === 0) return 0; const retried = recentLogs.filter(log => log.deliveryAttempt > 1 ).length; return (retried / recentLogs.length) * 100; } async checkHealth(webhookId) { const successRate = await this.calculateSuccessRate(webhookId, 60); const retryRate = await this.getRetryRate(webhookId, 60); const health = { webhookId, successRate, retryRate, status: 'healthy', issues: [] }; // Check success rate if (successRate < 95) { health.status = 'degraded'; health.issues.push({ type: 'low_success_rate', severity: successRate < 90 ? 'critical' : 'warning', message: `Success rate ${successRate.toFixed(2)}% is below threshold` }); } // Check retry rate if (retryRate > 10) { health.status = 'degraded'; health.issues.push({ type: 'high_retry_rate', severity: retryRate > 20 ? 'critical' : 'warning', message: `Retry rate ${retryRate.toFixed(2)}% is above threshold` }); } return health; } } // Usage const monitor = new WebhookMonitor(process.env.FN_ACCESS_TOKEN); async function runHealthCheck() { const webhookIds = ['wh_abc123', 'wh_def456']; for (const webhookId of webhookIds) { const health = await monitor.checkHealth(webhookId); console.log(`\nWebhook: ${webhookId}`); console.log(`Status: ${health.status}`); console.log(`Success Rate: ${health.successRate.toFixed(2)}%`); console.log(`Retry Rate: ${health.retryRate.toFixed(2)}%`); if (health.issues.length > 0) { console.log('Issues:'); health.issues.forEach(issue => { console.log(` - [${issue.severity}] ${issue.message}`); }); // Send alerts await sendAlert(health); } } } // Run every 5 minutes setInterval(runHealthCheck, 5 * 60 * 1000); ``` --- ## Application-Level Monitoring Track your webhook processing metrics: ### Prometheus Metrics ```javascript title="metrics.js" const prometheus = require('prom-client'); // Register default metrics prometheus.collectDefaultMetrics(); // Webhook-specific metrics const webhooksReceived = new prometheus.Counter({ name: 'webhooks_received_total', help: 'Total webhooks received', labelNames: ['event_name', 'webhook_id'] }); const webhooksProcessed = new prometheus.Counter({ name: 'webhooks_processed_total', help: 'Total webhooks processed', labelNames: ['event_name', 'status'] }); const webhookProcessingDuration = new prometheus.Histogram({ name: 'webhook_processing_duration_seconds', help: 'Webhook processing duration', labelNames: ['event_name'], buckets: [0.1, 0.5, 1, 2, 5, 10, 30] }); const webhookQueueSize = new prometheus.Gauge({ name: 'webhook_queue_size', help: 'Number of webhooks in processing queue' }); const webhookErrors = new prometheus.Counter({ name: 'webhook_errors_total', help: 'Total webhook processing errors', labelNames: ['event_name', 'error_type'] }); // Usage app.post('/webhooks/fieldnation', async (req, res) => { const payload = JSON.parse(req.body.toString()); webhooksReceived.inc({ event_name: payload.eventName, webhook_id: req.headers['x-fn-webhook-id'] }); // Queue and process... res.status(200).send('OK'); }); // Metrics endpoint app.get('/metrics', async (req, res) => { res.set('Content-Type', prometheus.register.contentType); res.end(await prometheus.register.metrics()); }); ``` ### Grafana Dashboard Create a Grafana dashboard with these panels: ``` # Success rate over 5 minutes sum(rate(webhooks_processed_total{status="success"}[5m])) / sum(rate(webhooks_processed_total[5m])) * 100 # Alert if < 95% ``` ``` # p95 processing latency histogram_quantile(0.95, sum(rate(webhook_processing_duration_seconds_bucket[5m])) by (le) ) # Alert if > 5 seconds ``` ``` # Errors per minute sum(rate(webhook_errors_total[1m])) by (error_type) # Alert if > 10/minute ``` ``` # Current queue size webhook_queue_size # Alert if > 1000 ``` --- ## Alerting Rules Set up alerts for critical conditions: ### Alert Definitions ```yaml - alert: WebhookSuccessRateLow expr: | sum(rate(webhooks_processed_total{status="success"}[5m])) / sum(rate(webhooks_processed_total[5m])) < 0.95 for: 5m labels: severity: warning annotations: summary: "Webhook success rate below 95%" description: "Success rate is {{ $value | humanizePercentage }} over the last 5 minutes" - alert: WebhookSuccessRateCritical expr: | sum(rate(webhooks_processed_total{status="success"}[5m])) / sum(rate(webhooks_processed_total[5m])) < 0.90 for: 2m labels: severity: critical annotations: summary: "Webhook success rate below 90%" description: "CRITICAL: Success rate is {{ $value | humanizePercentage }}" ``` ```yaml - alert: WebhookHighRetryRate expr: | sum(rate(webhooks_processed_total{status="retry"}[10m])) / sum(rate(webhooks_processed_total[10m])) > 0.10 for: 10m labels: severity: warning annotations: summary: "High webhook retry rate" description: "{{ $value | humanizePercentage }} of webhooks require retries" ``` ```yaml - alert: WebhookQueueBuildup expr: webhook_queue_size > 1000 for: 5m labels: severity: warning annotations: summary: "Webhook queue size growing" description: "Queue has {{ $value }} pending webhooks" - alert: WebhookQueueCritical expr: webhook_queue_size > 5000 for: 2m labels: severity: critical annotations: summary: "Webhook queue critically large" description: "CRITICAL: Queue has {{ $value }} pending webhooks" ``` ```yaml - alert: WebhookProcessingErrors expr: sum(rate(webhook_errors_total[5m])) > 10 for: 5m labels: severity: warning annotations: summary: "High webhook processing error rate" description: "{{ $value }} errors per minute" ``` ### Alert Destinations ```yaml # alertmanager.yml route: receiver: 'webhook-team' group_by: ['alertname', 'severity'] group_wait: 30s group_interval: 5m repeat_interval: 4h routes: - match: severity: critical receiver: 'pagerduty' - match: severity: warning receiver: 'slack' receivers: - name: 'webhook-team' email_configs: - to: 'webhooks-team@example.com' - name: 'pagerduty' pagerduty_configs: - service_key: '' - name: 'slack' slack_configs: - api_url: '' channel: '#webhooks-alerts' ``` --- ## Logging Best Practices ### Structured Logging ```javascript const winston = require('winston'); const logger = winston.createLogger({ level: 'info', format: winston.format.combine( winston.format.timestamp(), winston.format.json() ), defaultMeta: { service: 'webhook-processor' }, transports: [ new winston.transports.File({ filename: 'error.log', level: 'error' }), new winston.transports.File({ filename: 'combined.log' }) ] }); // Log webhook receipt logger.info('Webhook received', { eventId: payload.eventId, eventName: payload.eventName, workOrderId: payload.workOrderId, webhookId: req.headers['x-fn-webhook-id'], deliveryId: req.headers['x-fn-delivery-id'] }); // Log processing success logger.info('Webhook processed', { eventId: payload.eventId, duration: processingTime, status: 'success' }); // Log errors with context logger.error('Webhook processing failed', { eventId: payload.eventId, eventName: payload.eventName, error: error.message, stack: error.stack, retryable: isRetriable(error) }); ``` ### Log Aggregation Use tools like ELK Stack, Datadog, or CloudWatch: ```javascript // CloudWatch Logs const AWS = require('aws-sdk'); const cloudwatchlogs = new AWS.CloudWatchLogs(); async function logToCloudWatch(logGroupName, logStreamName, message) { await cloudwatchlogs.putLogEvents({ logGroupName, logStreamName, logEvents: [{ message: JSON.stringify(message), timestamp: Date.now() }] }).promise(); } ``` --- ## Dashboard Examples ### Real-Time Health Dashboard ```javascript title="health-dashboard.js" const express = require('express'); const app = express(); app.get('/health', async (req, res) => { const health = { status: 'healthy', timestamp: new Date().toISOString(), checks: { webhookEndpoint: await checkEndpoint(), queueHealth: await checkQueue(), deliveryLogs: await checkDeliveryLogs(), downstream: await checkDownstreamServices() } }; // Determine overall status const hasErrors = Object.values(health.checks).some(check => !check.healthy); health.status = hasErrors ? 'unhealthy' : 'healthy'; res.status(hasErrors ? 503 : 200).json(health); }); async function checkEndpoint() { try { // Check if endpoint is responsive const response = await fetch('https://your-endpoint.com/health'); return { healthy: response.ok, latency: response.headers.get('x-response-time') }; } catch (error) { return { healthy: false, error: error.message }; } } async function checkQueue() { const queueSize = await getQueueSize(); return { healthy: queueSize < 1000, queueSize, threshold: 1000 }; } async function checkDeliveryLogs() { const monitor = new WebhookMonitor(process.env.FN_ACCESS_TOKEN); const successRate = await monitor.calculateSuccessRate('wh_abc123', 60); return { healthy: successRate > 95, successRate, threshold: 95 }; } async function checkDownstreamServices() { // Check Salesforce, database, etc. return { healthy: true }; } app.listen(3001); ``` --- ## Debugging Tools ### Webhook Request Inspector ```javascript // Log incoming webhook details app.post('/webhooks/fieldnation/debug', express.raw({ type: 'application/json' }), (req, res) => { const debug = { timestamp: new Date().toISOString(), method: req.method, url: req.url, headers: req.headers, body: req.body.toString(), signature: req.headers['x-fn-signature'], webhookId: req.headers['x-fn-webhook-id'], eventName: req.headers['x-fn-event-name'], deliveryId: req.headers['x-fn-delivery-id'] }; console.log('=== WEBHOOK DEBUG ==='); console.log(JSON.stringify(debug, null, 2)); res.status(200).send('OK'); }); ``` ### Replay Failed Events ```javascript async function replayFailedEvents(webhookId, since) { const logs = await getDeliveryLogs(webhookId, { deliveryStatus: '500', sortBy: 'createdAt', sortDirection: 'DESC' }); for (const log of logs.result) { if (new Date(log.createdAt) < since) continue; console.log(`Retrying delivery: ${log.deliveryId}`); await fetch( `https://api-sandbox.fndev.net/api/v1/webhooks/delivery-logs/${log.deliveryId}/retry`, { method: 'PATCH', headers: { 'Authorization': `Bearer ${accessToken}` } } ); } } ``` --- ## Monitoring Checklist - ✅ Track delivery success rate (>99%) - ✅ Monitor processing latency (<5s p95) - ✅ Alert on high retry rates (>10%) - ✅ Watch queue depth (<100) - ✅ Track error rates by type - ✅ Monitor downstream service health - ✅ Set up automated health checks - ✅ Configure alerts to Slack/PagerDuty - ✅ Implement structured logging - ✅ Create dashboards for visibility --- --- ### Payload customization URL: /docs/webhooks/guides/payload-customization ## Overview Field Nation webhooks now support **customizable payloads** and **dynamic URLs**, giving you complete control over how webhook data is structured and delivered. ```mermaid graph LR A[Webhook Event] --> B{Has Custom Script?} B -->|No| C[Standard Payload] B -->|Yes| D[Transform Payload] D --> E[Apply URL Templates] E --> F[Deliver to Your System] C --> F ``` ### What You Can Do Restructure webhook data to match your system's expected format using JSONata expressions. Include event data in webhook URLs for intelligent routing (e.g., route by region or company). Send only the fields you need — reduce payload size and processing overhead. Match your system's naming conventions without building middleware. --- ## Why Customize Payloads? ### The Problem Without customization, integrating with external systems requires: 1. **Middleware servers** to transform Field Nation's payload format 2. **Extra development time** to map fields between systems 3. **Infrastructure costs** to maintain translation services 4. **Processing delays** from additional network hops ### The Solution With payload customization, transformations happen **at the source**: | Before | After | |--------|-------| | Build middleware to transform | Transform at delivery | | Maintain translation servers | No extra infrastructure | | One fixed payload format | Unlimited custom formats | | Static webhook URLs | Dynamic, data-driven URLs | --- ## Real-World Use Cases **Challenge:** ServiceNow expects incident tickets in a specific format with custom field names. **JSONata Script:** ```javascript { "u_external_ticket_id": $string($.workorder.id), "short_description": $.workorder.title, "state": $.workorder.status.name = "Work Done" ? "6" : "2", "assignment_group": "Field Services", "u_technician": $.workorder.assignee.first_name & " " & $.workorder.assignee.last_name, "u_client_company": $.workorder.company.name, "work_notes": "Status updated via Field Nation at " & $.timestamp } ``` **Result:** Direct webhook delivery to ServiceNow without middleware. **Challenge:** Route work orders to region-specific endpoints automatically. **Dynamic URL Template:** ```text https://{{$.workorder.location.country}}.api.company.com/workorders ``` **Result:** - US work orders → `us.api.company.com/workorders` - UK work orders → `uk.api.company.com/workorders` - CA work orders → `ca.api.company.com/workorders` **Challenge:** Send formatted notifications to Slack when work orders complete. **JSONata Script:** ```javascript { "text": "Work Order #" & $string($.workorder.id) & " - " & $.workorder.status.name, "blocks": [ { "type": "section", "text": { "type": "mrkdwn", "text": "*" & $.workorder.title & "*\nStatus: " & $.workorder.status.name & "\nTechnician: " & $.workorder.assignee.first_name & " " & $.workorder.assignee.last_name } } ] } ``` **Result:** Native Slack notifications without a middleware service. **Challenge:** A 15-year-old ERP only accepts flat JSON with uppercase field names. **JSONata Script:** ```javascript { "WORK_ORDER_ID": $.workorder.id, "WORK_ORDER_TITLE": $.workorder.title, "COMPANY_NAME": $.workorder.company.name, "TECHNICIAN_NAME": $.workorder.assignee.first_name & " " & $.workorder.assignee.last_name, "STATUS": $.workorder.status.name } ``` **Result:** Direct integration with legacy systems that can't be modified. **Challenge:** Data team needs webhook events in a format optimized for their data warehouse. **JSONata Script:** ```javascript { "event_type": $.event.name, "event_timestamp": $.timestamp, "workorder_id": $.workorder.id, "company_id": $.workorder.company.id, "status": $.workorder.status.name, "location_state": $.workorder.location.state } ``` **Result:** Webhook data flows directly into analytics pipeline without ETL processing. --- ## Transformation Options ### JSONata Expressions (Recommended) [JSONata](https://jsonata.org/) is a powerful query and transformation language for JSON data. > [INFO] **Why JSONata?** It's purpose-built for JSON transformation, has a concise syntax, and supports complex operations like filtering, sorting, and aggregation. #### Basic Field Selection ```javascript { "order_id": $.workorder.id, "title": $.workorder.title, "status": $.workorder.status.name } ``` #### Concatenating Fields ```javascript { "technician_name": $.workorder.assignee.first_name & " " & $.workorder.assignee.last_name, "location": $.workorder.location.city & ", " & $.workorder.location.state } ``` #### Conditional Logic ```javascript { "order_id": $.workorder.id, "is_complete": $.workorder.status.name = "Work Done", "priority": $.workorder.status.name = "At Risk" ? "high" : "normal", "state_code": $.workorder.status.name = "Work Done" ? "6" : $.workorder.status.name = "Approved" ? "7" : "2" } ``` #### Accessing Arrays ```javascript { "work_order_id": $.workorder.id, "po_number": $.workorder.custom_fields.results[name="PO Number"].value, "tags": $.workorder.tags.results.name, "has_priority_tag": "Priority" in $.workorder.tags.results.name } ``` #### Built-in Functions ```javascript { "work_order_id": $string($.workorder.id), "title_uppercase": $uppercase($.workorder.title), "timestamp": $now(), "scheduled_date": $substringBefore($.workorder.schedule.start.utc, "T") } ``` ### Static JSON Merge For simpler use cases, merge static fields into every webhook payload: ```json { "source": "field-nation", "environment": "production", "api_version": "3.0", "integration_id": "your-integration-id" } ``` This adds the static fields to the original payload without transforming it. --- ## Dynamic URL Templates Include values from the webhook payload in your endpoint URL using template expressions. ### Template Syntax ```text https://api.example.com/workorders/{{$.workorder.id}}/updates ``` Templates use `{{ expression }}` syntax where `expression` is a JSONata path. ### Common Patterns ```text https://api.example.com/workorders/{{$.workorder.id}}/webhooks ``` Delivers to: `https://api.example.com/workorders/12345/webhooks` ```text https://api.example.com/clients/{{$.workorder.company.id}}/events ``` Delivers to: `https://api.example.com/clients/100/events` ```text https://{{$.workorder.location.country}}.api.example.com/webhooks ``` Delivers to: `https://us.api.example.com/webhooks` ```text https://api.example.com/{{$.workorder.company.id}}/workorders/{{$.workorder.id}} ``` Delivers to: `https://api.example.com/100/workorders/12345` > **Restriction:** Templates are not allowed in the domain/origin part of the URL for security reasons. `https://{{$.malicious}}.example.com` is not permitted. --- ## Payload Field Reference All webhook events include these fields that you can use in transformations: ### Root Level | Path | Type | Description | |------|------|-------------| | `$.event.name` | string | Event name (e.g., `workorder.status.work_done`) | | `$.event.params` | object | Event-specific parameters | | `$.timestamp` | string | ISO 8601 timestamp when event occurred | | `$.triggered_by_user.id` | number | User ID who triggered the event | ### Work Order Core | Path | Type | Description | |------|------|-------------| | `$.workorder.id` | number | Unique work order ID | | `$.workorder.title` | string | Work order title | | `$.workorder.description` | string | Detailed description | | `$.workorder.status.id` | number | Status ID | | `$.workorder.status.name` | string | Status name | ### Company & People | Path | Type | Description | |------|------|-------------| | `$.workorder.company.id` | number | Buyer company ID | | `$.workorder.company.name` | string | Buyer company name | | `$.workorder.manager.first_name` | string | Manager first name | | `$.workorder.manager.last_name` | string | Manager last name | | `$.workorder.assignee.id` | number | Assigned provider ID | | `$.workorder.assignee.first_name` | string | Provider first name | | `$.workorder.assignee.last_name` | string | Provider last name | ### Location | Path | Type | Description | |------|------|-------------| | `$.workorder.location.city` | string | City | | `$.workorder.location.state` | string | State/Province | | `$.workorder.location.zip` | string | ZIP/Postal code | | `$.workorder.location.country` | string | Country code | ### Schedule & Pay | Path | Type | Description | |------|------|-------------| | `$.workorder.schedule.start.utc` | string | Scheduled start (UTC) | | `$.workorder.schedule.end.utc` | string | Scheduled end (UTC) | | `$.workorder.pay.type` | string | Pay type (fixed, hourly, etc.) | | `$.workorder.pay.base.amount` | number | Base pay amount | ### Custom Fields & Tags | Path | Type | Description | |------|------|-------------| | `$.workorder.custom_fields.results` | array | Custom field objects | | `$.workorder.custom_fields.results[n].name` | string | Field name | | `$.workorder.custom_fields.results[n].value` | string | Field value | | `$.workorder.tags.results` | array | Tag objects | | `$.workorder.tags.results[n].name` | string | Tag name | --- ## Setting Up Payload Customization ### Create or Edit a Webhook Navigate to [Webhooks Dashboard](https://app.fieldnation.com/integrations/webhooks) and create a new webhook or edit an existing one. ### Enable Payload Override Check the **"Override default payload"** option to enable customization. ### Select Script Language Choose your transformation approach: - **JSONata** (recommended) — Dynamic expressions for complex transformations - **JSON** — Static field merge for simple additions ### Write Your Transformation Script Enter your JSONata expression or JSON object: ```javascript { "ticket_id": $.workorder.id, "title": $.workorder.title, "status": $.workorder.status.name, "technician": $.workorder.assignee.first_name & " " & $.workorder.assignee.last_name } ``` ### Add Dynamic URL (Optional) If needed, update your webhook URL to include template expressions: ```text https://api.example.com/workorders/{{$.workorder.id}} ``` ### Save and Test Save the webhook configuration and trigger a test event to verify the transformation works correctly. --- ## Best Practices ### Start Simple Begin with basic field selection, then add complexity as needed: ```javascript // Start here { "id": $.workorder.id, "status": $.workorder.status.name } // Then expand { "id": $.workorder.id, "status": $.workorder.status.name, "company": $.workorder.company.name, "technician": $.workorder.assignee.first_name & " " & $.workorder.assignee.last_name } ``` ### Handle Missing Fields Use the coalesce operator to provide defaults: ```javascript { "assignee_name": $.workorder.assignee.first_name ? ($.workorder.assignee.first_name & " " & $.workorder.assignee.last_name) : "Unassigned" } ``` ### Test Before Production Use the webhook test feature to validate transformations before enabling in production. ### Keep Original Payloads Original payloads are automatically preserved in Field Nation for debugging and re-delivery. You can access them through the delivery logs. > [INFO] **Debugging Tip:** If a transformation fails, the original payload is still available in the delivery log details. Use this to diagnose and fix your script. --- ## Troubleshooting **Cause:** JSONata expression doesn't match payload structure. **Solution:** - Check field paths match the actual payload structure - Use `$.workorder.id` not `$.data.id` - Verify the event includes the fields you're accessing **Cause:** Template expression returns undefined or null. **Solution:** - Verify the field exists in the payload - Check for typos in the path - Use a fallback: `{{$.workorder.id ? $.workorder.id : 'default'}}` **Cause:** Invalid JSONata syntax. **Solution:** - Check for missing quotes around strings - Verify bracket matching - Test your expression at [try.jsonata.org](https://try.jsonata.org) **Cause:** Accessing fields that don't exist for this event. **Solution:** - Not all events include all fields (e.g., `assignee` is null before assignment) - Use conditional logic to handle missing data --- ## API Configuration When creating or updating webhooks via API, include the `webhookScript` field: ```bash curl -X POST https://api.fieldnation.com/api/v1/webhooks \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "url": "https://api.example.com/workorders/{{$.workorder.id}}", "method": "post", "status": "active", "events": ["workorder.status.work_done"], "webhookScript": { "scriptLanguage": "jsonata", "script": "{ \"id\": $.workorder.id, \"status\": $.workorder.status.name }", "enabled": true } }' ``` ### webhookScript Fields | Field | Type | Description | |-------|------|-------------| | `scriptLanguage` | string | `"jsonata"` or `"json"` | | `script` | string | The transformation script | | `enabled` | boolean | Toggle transformation on/off | --- ## Summary Payload customization eliminates the need for middleware by transforming data at the source: | Capability | Benefit | |------------|---------| | **JSONata transformations** | Restructure data to match any system | | **Dynamic URLs** | Route webhooks based on event content | | **Enable/disable toggle** | Test without deleting configurations | | **Original payload storage** | Debug with full context | **Bottom line:** Your webhooks, your format. No middleware required. --- ### Security URL: /docs/webhooks/guides/security ## Security Layers Implement multiple security layers for defense in depth: ```mermaid graph TD A[Incoming Webhook] -->|Layer 1| B{HTTPS?} B -->|No| C[Reject] B -->|Yes| D{IP Whitelisted?} D -->|No| E[Reject] D -->|Yes| F{Valid Signature?} F -->|No| G[Reject] F -->|Yes| H{Custom Auth?} H -->|No| I[Reject] H -->|Yes| J[Process Event] ``` --- ## 1. HTTPS Only **Critical**: Always use HTTPS endpoints. Field Nation rejects HTTP webhook URLs. ### Why HTTPS Matters - **Encryption**: Protects data in transit from eavesdropping - **Integrity**: Prevents man-in-the-middle attacks - **Authentication**: Verifies server identity ### SSL Certificate Requirements Your endpoint must have a valid SSL certificate: ✅ **Valid Certificates**: - Certificates from trusted CAs (Let's Encrypt, DigiCert, etc.) - Wildcard certificates (`*.example.com`) - Multi-domain certificates (SAN) ❌ **Invalid Certificates**: - Self-signed certificates - Expired certificates - Mismatched domain names ### Testing Your Certificate ```bash # Check SSL certificate validity curl -v https://your-endpoint.com/webhooks # Should see: # * SSL connection using TLSv1.3 # * Server certificate: # * subject: CN=your-endpoint.com # * issuer: CN=Let's Encrypt Authority # * SSL certificate verify ok ``` ### Free SSL with Let's Encrypt ```bash # Install certbot sudo apt-get install certbot # Get certificate sudo certbot certonly --standalone -d your-endpoint.com # Auto-renewal sudo certbot renew --dry-run ``` --- ## 2. Signature Verification (HMAC-SHA256) **Essential**: Always verify webhook signatures to ensure requests come from Field Nation. ### How It Works 1. Field Nation generates HMAC-SHA256 hash of request body using your webhook secret 2. Hash is sent in `x-fn-signature` header 3. You recompute the hash with your secret 4. Compare hashes using timing-safe comparison ### Signature Format ```plaintext x-fn-signature: sha256=abc123def456... ``` Format: `{algorithm}={hex_digest}` --- ### Implementation Examples ```javascript const crypto = require('crypto'); const express = require('express'); const app = express(); // IMPORTANT: Use raw body parser app.use(express.raw({ type: 'application/json' })); function verifyWebhookSignature(rawBody, signature, secret) { if (!signature) { return false; } // Parse algorithm and hash from signature const [algorithm, requestHash] = signature.split('='); if (algorithm !== 'sha256') { return false; } // Compute expected hash const expectedHash = crypto .createHmac('sha256', secret) .update(rawBody) .digest('hex'); // Timing-safe comparison try { return crypto.timingSafeEqual( Buffer.from(expectedHash, 'hex'), Buffer.from(requestHash, 'hex') ); } catch { return false; } } app.post('/webhooks/fieldnation', (req, res) => { const signature = req.headers['x-fn-signature']; const secret = process.env.WEBHOOK_SECRET; // Verify signature if (!verifyWebhookSignature(req.body, signature, secret)) { console.error('Invalid webhook signature'); return res.status(401).send('Unauthorized'); } // Signature valid, process webhook const payload = JSON.parse(req.body.toString()); console.log('Verified webhook:', payload.eventName); res.status(200).send('OK'); }); app.listen(3000); ``` > **Critical**: Use raw body (`Buffer`) for signature verification, not parsed JSON. Parsing changes formatting and breaks signature validation. ```python import os import hmac import hashlib from flask import Flask, request app = Flask(__name__) def verify_webhook_signature(body, signature, secret): if not signature: return False # Parse algorithm and hash try: algorithm, request_hash = signature.split('=') except ValueError: return False if algorithm != 'sha256': return False # Compute expected hash expected_hash = hmac.new( secret.encode('utf-8'), body, hashlib.sha256 ).hexdigest() # Timing-safe comparison return hmac.compare_digest(expected_hash, request_hash) @app.route('/webhooks/fieldnation', methods=['POST']) def handle_webhook(): signature = request.headers.get('x-fn-signature') secret = os.environ['WEBHOOK_SECRET'] body = request.get_data() # Verify signature if not verify_webhook_signature(body, signature, secret): print('Invalid webhook signature') return 'Unauthorized', 401 # Signature valid, process webhook payload = request.get_json() print(f"Verified webhook: {payload['eventName']}") return 'OK', 200 if __name__ == '__main__': app.run(port=3000) ``` ```php ``` ```go package main import ( "crypto/hmac" "crypto/sha256" "crypto/subtle" "encoding/hex" "io" "log" "net/http" "os" "strings" ) func verifyWebhookSignature(body []byte, signature, secret string) bool { if signature == "" { return false } // Parse algorithm and hash parts := strings.SplitN(signature, "=", 2) if len(parts) != 2 { return false } algorithm, requestHash := parts[0], parts[1] if algorithm != "sha256" { return false } // Compute expected hash h := hmac.New(sha256.New, []byte(secret)) h.Write(body) expectedHash := hex.EncodeToString(h.Sum(nil)) // Timing-safe comparison return subtle.ConstantTimeCompare( []byte(expectedHash), []byte(requestHash), ) == 1 } func handleWebhook(w http.ResponseWriter, r *http.Request) { signature := r.Header.Get("x-fn-signature") secret := os.Getenv("WEBHOOK_SECRET") // Read body body, err := io.ReadAll(r.Body) if err != nil { http.Error(w, "Bad request", http.StatusBadRequest) return } // Verify signature if !verifyWebhookSignature(body, signature, secret) { log.Println("Invalid webhook signature") http.Error(w, "Unauthorized", http.StatusUnauthorized) return } // Signature valid, process webhook log.Println("Verified webhook") w.WriteHeader(http.StatusOK) w.Write([]byte("OK")) } func main() { http.HandleFunc("/webhooks/fieldnation", handleWebhook) log.Fatal(http.Listen AndServe(":3000", nil)) } ``` ```ruby require 'sinatra' require 'openssl' require 'json' def verify_webhook_signature(body, signature, secret) return false if signature.nil? || signature.empty? # Parse algorithm and hash algorithm, request_hash = signature.split('=', 2) return false if algorithm != 'sha256' # Compute expected hash expected_hash = OpenSSL::HMAC.hexdigest( 'sha256', secret, body ) # Timing-safe comparison Rack::Utils.secure_compare(expected_hash, request_hash) end post '/webhooks/fieldnation' do signature = request.env['HTTP_X_FN_SIGNATURE'] secret = ENV['WEBHOOK_SECRET'] body = request.body.read # Verify signature unless verify_webhook_signature(body, signature, secret) puts 'Invalid webhook signature' halt 401, 'Unauthorized' end # Signature valid, process webhook payload = JSON.parse(body) puts "Verified webhook: #{payload['eventName']}" status 200 body 'OK' end ``` ### Common Signature Verification Mistakes **Wrong**: ```javascript app.use(express.json()); const hash = crypto.createHmac('sha256', secret) .update(JSON.stringify(req.body)) // ❌ Won't match .digest('hex'); ``` **Correct**: ```javascript app.use(express.raw({ type: 'application/json' })); const hash = crypto.createHmac('sha256', secret) .update(req.body) // ✅ Raw buffer .digest('hex'); ``` **Wrong**: ```javascript if (expectedHash === requestHash) { // ❌ Vulnerable to timing attacks return true; } ``` **Correct**: ```javascript return crypto.timingSafeEqual( // ✅ Timing-safe Buffer.from(expectedHash), Buffer.from(requestHash) ); ``` **Wrong**: ```python # request_hash is hex string, but comparing with bytes return expected_hash == request_hash.encode() ``` **Correct**: ```python # Both as hex strings expected_hash = hmac.new(secret, body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected_hash, request_hash) ``` --- ## 3. IP Whitelisting Restrict webhook delivery to Field Nation's IP addresses for an additional security layer. ### Field Nation IP Addresses ```plaintext 44.225.211.232 44.237.253.26 ``` ```plaintext 3.226.5.230 34.198.172.230 ``` > **Important**: These IPs may change. Subscribe to Field Nation's infrastructure updates or check documentation regularly. ### Implementation Examples ```nginx # /etc/nginx/sites-available/your-site geo $is_fieldnation { default 0; # Sandbox IPs 44.225.211.232 1; 44.237.253.26 1; # Production IPs 3.226.5.230 1; 34.198.172.230 1; } server { listen 443 ssl; server_name your-endpoint.com; location /webhooks/fieldnation { if ($is_fieldnation = 0) { return 403; } proxy_pass http://localhost:3000; } } ``` ```apache # /etc/apache2/sites-available/your-site.conf ServerName your-endpoint.com # Require Field Nation IPs Require ip 44.225.211.232 Require ip 44.237.253.26 Require ip 3.226.5.230 Require ip 34.198.172.230 ``` ```bash # AWS CLI - Create security group rule # Sandbox IPs aws ec2 authorize-security-group-ingress \ --group-id sg-xxx \ --protocol tcp \ --port 443 \ --cidr 44.225.211.232/32 aws ec2 authorize-security-group-ingress \ --group-id sg-xxx \ --protocol tcp \ --port 443 \ --cidr 44.237.253.26/32 # Production IPs aws ec2 authorize-security-group-ingress \ --group-id sg-xxx \ --protocol tcp \ --port 443 \ --cidr 3.226.5.230/32 aws ec2 authorize-security-group-ingress \ --group-id sg-xxx \ --protocol tcp \ --port 443 \ --cidr 34.198.172.230/32 ``` ```javascript const express = require('express'); const app = express(); const FIELDNATION_IPS = [ '44.225.211.232', // Sandbox '44.237.253.26', // Sandbox '3.226.5.230', // Production '34.198.172.230' // Production ]; function ipWhitelist(req, res, next) { // Get client IP (handle X-Forwarded-For if behind proxy) const clientIP = req.headers['x-forwarded-for']?.split(',')[0].trim() || req.socket.remoteAddress; // Check if IP is whitelisted if (!FIELDNATION_IPS.includes(clientIP)) { console.error(`Rejected request from non-whitelisted IP: ${clientIP}`); return res.status(403).send('Forbidden'); } next(); } // Apply to webhook endpoints app.use('/webhooks/fieldnation', ipWhitelist); app.post('/webhooks/fieldnation', (req, res) => { // Process webhook res.status(200).send('OK'); }); ``` --- ## 4. Custom Authentication Headers Add your own authentication layer using custom headers configured in the webhook. ### Setting Up Custom Headers When creating the webhook: ```json { "url": "https://your-endpoint.com/webhooks", "webhookAttribute": { "header": { "Authorization": "Bearer your-secret-api-token", "X-API-Key": "your-api-key", "X-Webhook-Secret": "additional-secret" } } } ``` ### Validating Custom Headers ```javascript function validateCustomAuth(req) { const authHeader = req.headers['authorization']; const apiKey = req.headers['x-api-key']; // Validate Authorization header if (authHeader !== `Bearer ${process.env.API_TOKEN}`) { return false; } // Validate API key if (apiKey !== process.env.API_KEY) { return false; } return true; } app.post('/webhooks/fieldnation', (req, res) => { // Verify signature first if (!verifySignature(req.body, req.headers['x-fn-signature'])) { return res.status(401).send('Invalid signature'); } // Then check custom auth if (!validateCustomAuth(req)) { return res.status(401).send('Invalid authentication'); } // All checks passed, process webhook res.status(200).send('OK'); }); ``` > [INFO] **Best Practice**: Use custom headers for additional authentication, but **always** verify Field Nation's signature first. Custom headers alone are not sufficient security. --- ## 5. Rate Limiting Protect your endpoint from potential abuse with rate limiting: ###Express Rate Limit ```javascript const rateLimit = require('express-rate-limit'); const webhookLimiter = rateLimit({ windowMs: 1 * 60 * 1000, // 1 minute max: 100, // 100 requests per minute message: 'Too many webhook requests', standardHeaders: true, legacyHeaders: false }); app.use('/webhooks/fieldnation', webhookLimiter); ``` ### nginx Rate Limiting ```nginx # Define rate limit zone (10 req/sec) limit_req_zone $binary_remote_addr zone=webhook_limit:10m rate=10r/s; server { location /webhooks/fieldnation { # Apply rate limit limit_req zone=webhook_limit burst=20 nodelay; proxy_pass http://localhost:3000; } } ``` --- ## 6. Logging & Monitoring Log security events for auditing and incident response: ### What to Log ```javascript function logSecurityEvent(type, details) { const event = { timestamp: new Date().toISOString(), type, ...details }; console.log(JSON.stringify(event)); // Send to monitoring system if (type === 'security_violation') { alertSecurityTeam(event); } } app.post('/webhooks/fieldnation', (req, res) => { const clientIP = req.socket.remoteAddress; // Log all webhook attempts logSecurityEvent('webhook_attempt', { ip: clientIP, eventName: req.headers['x-fn-event-name'] }); // Verify signature if (!verifySignature(req.body, req.headers['x-fn-signature'])) { logSecurityEvent('security_violation', { reason: 'invalid_signature', ip: clientIP }); return res.status(401).send('Unauthorized'); } // Log successful verification logSecurityEvent('webhook_verified', { ip: clientIP, eventId: JSON.parse(req.body.toString()).eventId }); res.status(200).send('OK'); }); ``` ### What NOT to Log > **Never log**: - Webhook secrets - Raw request bodies (may contain PII) - Authorization tokens - Payment information - Personal identifying information (PII) --- ## Complete Security Implementation Here's a production-ready example with all security layers: ```javascript title="secure-webhook-handler.js" const express = require('express'); const crypto = require('crypto'); const rateLimit = require('express-rate-limit'); const app = express(); // Configuration const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET; const API_TOKEN = process.env.API_TOKEN; const FIELDNATION_IPS = [ '44.225.211.232', '44.237.253.26', '3.226.5.230', '34.198.172.230' ]; // Rate limiting const webhookLimiter = rateLimit({ windowMs: 60 * 1000, max: 100, message: 'Too many requests' }); // Raw body parser (for signature verification) app.use(express.raw({ type: 'application/json' })); // IP whitelist middleware function ipWhitelist(req, res, next) { const clientIP = req.headers['x-forwarded-for']?.split(',')[0].trim() || req.socket.remoteAddress; if (!FIELDNATION_IPS.includes(clientIP)) { console.error(`Rejected: Non-whitelisted IP ${clientIP}`); return res.status(403).send('Forbidden'); } next(); } // Signature verification function verifySignature(rawBody, signature, secret) { if (!signature) return false; const [algorithm, requestHash] = signature.split('='); if (algorithm !== 'sha256') return false; const expectedHash = crypto .createHmac('sha256', secret) .update(rawBody) .digest('hex'); try { return crypto.timingSafeEqual( Buffer.from(expectedHash), Buffer.from(requestHash) ); } catch { return false; } } // Custom auth validation function validateCustomAuth(req) { const authHeader = req.headers['authorization']; return authHeader === `Bearer ${API_TOKEN}`; } // Webhook endpoint with all security layers app.post('/webhooks/fieldnation', webhookLimiter, // Layer 1: Rate limiting ipWhitelist, // Layer 2: IP whitelist (req, res) => { try { // Layer 3: Signature verification const signature = req.headers['x-fn-signature']; if (!verifySignature(req.body, signature, WEBHOOK_SECRET)) { console.error('Invalid signature'); return res.status(401).send('Unauthorized'); } // Layer 4: Custom authentication if (!validateCustomAuth(req)) { console.error('Invalid custom authentication'); return res.status(401).send('Unauthorized'); } // All security checks passed const payload = JSON.parse(req.body.toString()); console.log(`Verified webhook: ${payload.eventName}`); // Respond immediately res.status(200).send('OK'); // Process asynchronously processWebhookAsync(payload); } catch (error) { console.error('Webhook processing error:', error); res.status(500).send('Internal error'); } } ); async function processWebhookAsync(payload) { // Your business logic here } app.listen(3000, () => { console.log('Secure webhook server running on port 3000'); }); ``` --- ## Security Checklist Before going to production: - ✅ HTTPS with valid SSL certificate - ✅ Signature verification implemented - ✅ IP whitelisting configured - ✅ Custom authentication headers (optional but recommended) - ✅ Rate limiting enabled - ✅ Security event logging - ✅ Monitoring and alerts set up - ✅ Secrets stored securely (not in code) - ✅ Error handling doesn't leak sensitive info - ✅ Regular security audits scheduled --- --- ### Testing URL: /docs/webhooks/guides/testing ## Testing Strategy ```mermaid graph TD A[Development] --> B[Local Testing] B --> C[ngrok/Localtunnel] B --> D[Request Inspectors] B --> E[Mock Payloads] A --> F[Staging] F --> G[Full Integration Test] F --> H[Load Testing] F --> I[Production] I --> J[Canary Deployment] I --> K[Smoke Tests] ``` --- ## Local Testing Tools ### 1. ngrok (Recommended) **Best for**: Testing with real Field Nation webhooks locally #### Setup ```bash # Install ngrok brew install ngrok/ngrok/ngrok # Or download from https://ngrok.com/download # Authenticate ngrok authtoken YOUR_NGROK_TOKEN # Start tunnel to local port ngrok http 3000 ``` **Output:** ```plaintext Session Status online Account your-account (Plan: Free) Version 3.x.x Region United States (us) Latency - Web Interface http://127.0.0.1:4040 Forwarding https://abc123.ngrok-free.app -> http://localhost:3000 Connections ttl opn rt1 rt5 p50 p90 0 0 0.00 0.00 0.00 0.00 ``` #### Using ngrok URL 1. **Copy the HTTPS URL**: `https://abc123.ngrok-free.app` 2. **Create webhook** with URL: `https://abc123.ngrok-free.app/webhooks/fieldnation` 3. **Test**: Trigger events in Field Nation sandbox > [INFO] **Web Interface**: Visit `http://127.0.0.1:4040` to see all requests in real-time, including headers and payloads. #### Advanced ngrok Configuration ```yaml title="ngrok.yml" version: "2" authtoken: YOUR_NGROK_TOKEN tunnels: webhook: proto: http addr: 3000 hostname: your-custom-domain.ngrok-free.app inspect: true bind_tls: true ``` Start named tunnel: ```bash ngrok start webhook ``` --- ### 2. localtunnel **Alternative to ngrok** - no account required ```bash # Install npm install -g localtunnel # Start tunnel lt --port 3000 --subdomain my-webhook # Output: https://my-webhook.loca.lt ``` --- ### 3. Request Inspectors Perfect for viewing webhook payloads without writing code. #### webhook.site 1. Visit [webhook.site](https://webhook.site) 2. Copy your unique URL 3. Create webhook with that URL 4. View requests in real-time **Features:** - No signup required - See headers, body, query params - Custom responses - Request history #### RequestBin / Request.bin Similar to webhook.site: 1. Visit [requestbin.com](https://requestbin.com) 2. Create a bin 3. Use bin URL as webhook endpoint 4. View all requests --- ## Local Development Setup ### Complete Testing Environment ```javascript title="test-webhook-server.js" const express = require('express'); const crypto = require('crypto'); const app = express(); // Environment const PORT = process.env.PORT || 3000; const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET || 'test-secret'; // Middleware app.use(express.raw({ type: 'application/json' })); // Health check app.get('/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString() }); }); // Webhook endpoint with full logging app.post('/webhooks/fieldnation', (req, res) => { console.log('\n=== WEBHOOK RECEIVED ==='); console.log('Timestamp:', new Date().toISOString()); // Log headers console.log('\nHeaders:'); Object.entries(req.headers).forEach(([key, value]) => { if (key.startsWith('x-fn-') || key === 'content-type') { console.log(` ${key}: ${value}`); } }); // Verify signature const signature = req.headers['x-fn-signature']; const isValid = verifySignature(req.body, signature, WEBHOOK_SECRET); console.log(`\nSignature Valid: ${isValid}`); if (!isValid) { console.log('❌ SIGNATURE VERIFICATION FAILED'); return res.status(401).send('Unauthorized'); } // Parse payload const payload = JSON.parse(req.body.toString()); // Log event details console.log('\nEvent Details:'); console.log(` Event ID: ${payload.eventId}`); console.log(` Event Name: ${payload.eventName}`); console.log(` Work Order ID: ${payload.workOrderId}`); console.log(` Timestamp: ${payload.timestamp}`); // Log payload preview console.log('\nPayload Preview:'); console.log(JSON.stringify(payload, null, 2).substring(0, 500) + '...'); console.log('\n✅ Webhook processed successfully'); console.log('=======================\n'); // Respond res.status(200).send('OK'); }); 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) ); } app.listen(PORT, () => { console.log(`🚀 Webhook test server running on port ${PORT}`); console.log(`📝 Health check: http://localhost:${PORT}/health`); console.log(`📨 Webhook endpoint: http://localhost:${PORT}/webhooks/fieldnation`); console.log(`\n🔐 Using secret: ${WEBHOOK_SECRET}`); console.log('\n⏳ Waiting for webhooks...\n'); }); ``` **Run:** ```bash # Set secret (get from webhook creation) export WEBHOOK_SECRET=your-webhook-secret # Start server node test-webhook-server.js # In another terminal, start ngrok ngrok http 3000 ``` --- ## Mock Webhook Payloads Test your processing logic with realistic payloads: ### Work Order Published Event ```json title="mock-payloads/workorder-published.json" { "eventName": "workorder.status.published", "eventId": "evt_test_001", "workOrderId": 99999, "timestamp": "2026-01-15T10:30:00.000Z", "data": { "id": 99999, "title": "Test Router Installation", "description": "This is a test work order for webhook testing", "status": "published", "type": "installation", "schedule": { "serviceWindow": { "start": "2026-01-20T09:00:00Z", "end": "2026-01-20T17:00:00Z", "mode": "hours" } }, "location": { "address1": "123 Test Street", "city": "San Francisco", "state": "CA", "zip": "94105", "coordinates": { "latitude": 37.7749, "longitude": -122.4194 } }, "pay": { "type": "fixed", "amount": 250.00, "currency": "USD" }, "buyer": { "id": 456, "name": "Test Company", "companyId": 789 }, "tags": ["test", "router"], "customFields": { "testField": "test-value" }, "createdAt": "2026-01-15T10:00:00Z", "updatedAt": "2026-01-15T10:30:00Z" } } ``` ### Work Order Assigned Event ```json title="mock-payloads/workorder-assigned.json" { "eventName": "workorder.status.assigned", "eventId": "evt_test_002", "workOrderId": 99999, "timestamp": "2026-01-15T11:00:00.000Z", "data": { "id": 99999, "title": "Test Router Installation", "status": "assigned", "provider": { "id": 12345, "name": "John Test Provider", "userId": 67890, "rating": 4.8, "completedJobs": 150 }, "schedule": { "serviceWindow": { "start": "2026-01-20T09:00:00Z", "end": "2026-01-20T17:00:00Z" } } } } ``` ### Send Mock Payload ```bash title="send-mock-webhook.sh" #!/bin/bash WEBHOOK_URL="http://localhost:3000/webhooks/fieldnation" WEBHOOK_SECRET="your-webhook-secret" PAYLOAD_FILE="mock-payloads/workorder-published.json" # Read payload PAYLOAD=$(cat $PAYLOAD_FILE) # Generate signature SIGNATURE=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" | awk '{print $2}') # Send request curl -X POST "$WEBHOOK_URL" \ -H "Content-Type: application/json" \ -H "x-fn-signature: sha256=$SIGNATURE" \ -H "x-fn-webhook-id: wh_test_123" \ -H "x-fn-event-name: workorder.status.published" \ -H "x-fn-delivery-id: del_test_001" \ -H "x-fn-timestamp: $(date -u +"%Y-%m-%dT%H:%M:%SZ")" \ -d "$PAYLOAD" \ -v ``` **Make executable and run:** ```bash chmod +x send-mock-webhook.sh ./send-mock-webhook.sh ``` ### Node.js Mock Sender ```javascript title="send-mock-webhook.js" const crypto = require('crypto'); const fetch = require('node-fetch'); const fs = require('fs'); async function sendMockWebhook(payloadPath, webhookUrl, secret) { // Read payload const payload = JSON.parse(fs.readFileSync(payloadPath, 'utf8')); const payloadString = JSON.stringify(payload); // Generate signature const signature = crypto .createHmac('sha256', secret) .update(payloadString) .digest('hex'); // Send request const response = await fetch(webhookUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-fn-signature': `sha256=${signature}`, 'x-fn-webhook-id': 'wh_test_123', 'x-fn-event-name': payload.eventName, 'x-fn-delivery-id': `del_test_${Date.now()}`, 'x-fn-timestamp': new Date().toISOString() }, body: payloadString }); console.log('Response Status:', response.status); console.log('Response Body:', await response.text()); } // Usage sendMockWebhook( './mock-payloads/workorder-published.json', 'http://localhost:3000/webhooks/fieldnation', 'your-webhook-secret' ); ``` --- ## Testing Strategies by Environment ### Development Environment **Goal**: Rapid iteration and debugging ### Use Request Inspector Start with webhook.site to see raw payloads without writing code. ### Local Server + ngrok Once you understand the payload structure, build your handler locally. ### Mock Payloads Test specific scenarios without triggering real events. ### Signature Verification Always test signature validation logic. ### Error Scenarios Test invalid signatures, malformed payloads, timeouts. --- ### Staging Environment **Goal**: Full integration testing before production ```javascript title="staging-test-suite.js" const assert = require('assert'); describe('Webhook Integration Tests', () => { it('should receive and process work order published event', async () => { // 1. Create test work order in sandbox const workOrder = await createTestWorkOrder(); // 2. Publish work order (triggers webhook) await publishWorkOrder(workOrder.id); // 3. Wait for webhook processing await sleep(5000); // 4. Verify webhook was received const logs = await getProcessedEvents(); const event = logs.find(log => log.workOrderId === workOrder.id && log.eventName === 'workorder.status.published' ); assert.ok(event, 'Webhook event not found'); assert.equal(event.status, 'processed'); // 5. Verify downstream effects const salesforceRecord = await getSalesforceRecord(workOrder.id); assert.ok(salesforceRecord, 'Work order not synced to Salesforce'); }); it('should handle duplicate webhooks idempotently', async () => { const payload = getMockPayload(); // Send same webhook twice await sendWebhook(payload); await sendWebhook(payload); // Verify processed only once const processCount = await getProcessingCount(payload.eventId); assert.equal(processCount, 1, 'Webhook processed more than once'); }); it('should retry failed processing', async () => { // Temporarily break downstream service await disableSalesforceSync(); // Send webhook const payload = getMockPayload(); await sendWebhook(payload); // Verify failed await sleep(2000); let status = await getProcessingStatus(payload.eventId); assert.equal(status, 'failed'); // Fix service await enableSalesforceSync(); // Verify retry succeeded await sleep(15000); // Wait for retry status = await getProcessingStatus(payload.eventId); assert.equal(status, 'processed'); }); }); ``` **Run staging tests:** ```bash # Set staging environment export NODE_ENV=staging export WEBHOOK_URL=https://staging.example.com/webhooks export FN_CLIENT_ID=staging-client-id export FN_CLIENT_SECRET=staging-secret # Run tests npm test ``` --- ### Production Environment **Goal**: Verify production deployment without disrupting operations #### Smoke Tests ```javascript title="production-smoke-test.js" async function runSmokeTests() { const tests = { webhookEndpoint: false, signatureVerification: false, queueHealth: false, downstreamServices: false }; // Test 1: Webhook endpoint responsive try { const response = await fetch('https://api.example.com/health'); tests.webhookEndpoint = response.ok; } catch (error) { console.error('Webhook endpoint test failed:', error); } // Test 2: Signature verification working try { const testPayload = getMockPayload(); const signature = generateSignature(testPayload, process.env.WEBHOOK_SECRET); const response = await sendWebhook(testPayload, signature); tests.signatureVerification = response.status === 200; } catch (error) { console.error('Signature test failed:', error); } // Test 3: Queue healthy try { const queueSize = await getQueueSize(); tests.queueHealth = queueSize < 1000; } catch (error) { console.error('Queue health test failed:', error); } // Test 4: Downstream services accessible try { await Promise.all([ checkSalesforce(), checkDatabase(), checkRedis() ]); tests.downstreamServices = true; } catch (error) { console.error('Downstream services test failed:', error); } // Report const allPassed = Object.values(tests).every(t => t); console.log('\n=== Smoke Test Results ==='); Object.entries(tests).forEach(([test, passed]) => { console.log(` ${passed ? '✅' : '❌'} ${test}`); }); console.log(`\nOverall: ${allPassed ? '✅ PASSED' : '❌ FAILED'}`); return allPassed; } // Run after deployment runSmokeTests().then(passed => { process.exit(passed ? 0 : 1); }); ``` #### Canary Deployment ```javascript // Create separate webhook for testing const canaryWebhook = await createWebhook({ url: 'https://api-canary.example.com/webhooks', status: 'active', events: ['workorder.status.published'], // Single event for testing notificationEmail: 'canary-alerts@example.com' }); // Monitor canary for issues await monitorCanary(canaryWebhook.webhookId, { duration: '1 hour', successRateThreshold: 99, onSuccess: () => { console.log('Canary successful, promoting to production'); // Update main webhook to new version }, onFailure: () => { console.log('Canary failed, rolling back'); // Deactivate canary, keep old version } }); ``` --- ## Load Testing Test webhook handling under high volume: ```javascript title="load-test.js" const autocannon = require('autocannon'); async function loadTest() { const result = await autocannon({ url: 'http://localhost:3000/webhooks/fieldnation', connections: 100, // Concurrent connections duration: 30, // 30 seconds pipelining: 1, method: 'POST', headers: { 'Content-Type': 'application/json', 'x-fn-signature': 'sha256=test-signature', 'x-fn-webhook-id': 'wh_load_test', 'x-fn-event-name': 'workorder.status.published' }, body: JSON.stringify(getMockPayload()) }); console.log('\n=== Load Test Results ==='); console.log(`Requests: ${result.requests.total}`); console.log(`Duration: ${result.duration}s`); console.log(`RPS: ${result.requests.average}`); console.log(`Latency p50: ${result.latency.p50}ms`); console.log(`Latency p95: ${result.latency.p95}ms`); console.log(`Latency p99: ${result.latency.p99}ms`); console.log(`Errors: ${result.errors}`); console.log(`Timeouts: ${result.timeouts}`); } loadTest(); ``` --- ## Testing Checklist ### Local Testing - ✅ Set up local server with logging - ✅ Use ngrok for public URL - ✅ Test with request inspector first - ✅ Verify signature validation - ✅ Test with mock payloads - ✅ Test error scenarios ### Staging Testing - ✅ Full integration tests - ✅ Test all event types - ✅ Verify idempotency - ✅ Test retry logic - ✅ Load testing - ✅ Monitor for 24 hours ### Production Testing - ✅ Smoke tests after deployment - ✅ Canary deployment - ✅ Monitor metrics closely - ✅ Have rollback plan ready - ✅ Test error handling - ✅ Verify monitoring/alerts --- --- ### Common issues URL: /docs/webhooks/troubleshooting/common-issues ## Signature Verification Failures ### Issue ``` ❌ Signature verification failed ❌ Unauthorized (401) ``` ### Causes & Solutions **Solution**: Verify you're using the correct webhook secret. ```javascript // ❌ Wrong - using wrong secret const secret = 'wrong-secret'; // ✅ Correct - use secret from webhook creation const secret = process.env.WEBHOOK_SECRET; // From webhook creation response ``` **Solution**: Verify signature using raw request body, before parsing. ```javascript // ❌ Wrong - body already parsed app.use(express.json()); app.post('/webhooks', (req, res) => { verifySignature(req.body, signature, secret); // Will fail! }); // ✅ Correct - use raw body app.post('/webhooks', express.raw({type: 'application/json'}), (req, res) => { verifySignature(req.body, signature, secret); // Works! }); ``` **Solution**: Extract hash from `sha256=...` format. ```javascript // ❌ Wrong - using full signature string const valid = expectedHash === req.headers['x-fn-signature']; // ✅ Correct - extract hash portion const [algorithm, hash] = req.headers['x-fn-signature'].split('='); const valid = expectedHash === hash; ``` **Solution**: Ensure raw bytes are used for verification. ```javascript // ✅ Correct approach const signature = req.headers['x-fn-signature']; const [algorithm, providedHash] = signature.split('='); const expectedHash = crypto .createHmac(algorithm, secret) .update(req.body) // Raw buffer .digest('hex'); const valid = crypto.timingSafeEqual( Buffer.from(expectedHash), Buffer.from(providedHash) ); ``` [Complete security guide →](/docs/webhooks/guides/security) --- ## Webhooks Not Arriving ### Issue ``` ❌ No webhooks received ❌ Events triggering but no delivery ``` ### Diagnostic Steps ### Check Webhook Status ```bash curl -X GET https://api-sandbox.fndev.net/api/v1/webhooks/wh_abc123 \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` Verify `status: "active"` in response. ### Test Endpoint Accessibility ```bash curl -X POST https://your-endpoint.com/webhooks \ -H "Content-Type: application/json" \ -d '{"test": "data"}' ``` Should return 200 OK. ### Check SSL Certificate ```bash curl -v https://your-endpoint.com/webhooks ``` Look for `SSL certificate verify ok`. ### Review Delivery Logs ```bash curl -X GET "https://api-sandbox.fndev.net/api/v1/webhooks/delivery-logs?webhookId=wh_abc123" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` Look for error patterns in failed deliveries. ### Verify Event Subscription Ensure webhook is subscribed to the events you're triggering: ```javascript const webhook = await getWebhook('wh_abc123'); console.log('Subscribed events:', webhook.result.events); ``` --- ## Timeout Errors ### Issue ``` ❌ Request timeout after 30 seconds ❌ Connection timeout ``` ### Solution Respond immediately, process asynchronously: ```javascript // ❌ Wrong - slow synchronous processing app.post('/webhooks', async (req, res) => { await verifySignature(req); await syncToSalesforce(req.body); // Slow operation await updateDatabase(req.body); // Slow operation await sendEmail(req.body); // Slow operation res.status(200).send('OK'); // Too late! }); // ✅ Correct - respond immediately app.post('/webhooks', async (req, res) => { await verifySignature(req); // Respond immediately res.status(200).send('OK'); // Process asynchronously processWebhookAsync(req.body) .catch(error => console.error('Processing error:', error)); }); ``` [Complete handling guide →](/docs/webhooks/guides/handling-events) --- ## Duplicate Events ### Issue ``` ❌ Same event received multiple times ❌ Duplicate processing ``` ### Solution Implement idempotency using `eventId`: ```javascript const processedEvents = new Set(); // Or Redis/Database app.post('/webhooks', async (req, res) => { const payload = JSON.parse(req.body.toString()); const { eventId } = payload; // Check if already processed if (processedEvents.has(eventId)) { console.log(`Duplicate event ${eventId}, skipping`); return res.status(200).send('Already processed'); } // Mark as processing processedEvents.add(eventId); try { // Process event await handleEvent(payload); res.status(200).send('OK'); } catch (error) { // Remove on failure to allow retry processedEvents.delete(eventId); throw error; } }); ``` [Complete idempotency guide →](/docs/webhooks/guides/handling-events) --- ## JSON Parsing Errors ### Issue ``` ❌ SyntaxError: Unexpected token ❌ Invalid JSON ``` ### Solution Handle raw body correctly: ```javascript // For signature verification app.use(express.raw({ type: 'application/json' })); app.post('/webhooks', (req, res) => { try { // 1. Verify signature with raw body const signature = req.headers['x-fn-signature']; verifySignature(req.body, signature, secret); // 2. Parse JSON const payload = JSON.parse(req.body.toString()); // 3. Process processWebhook(payload); res.status(200).send('OK'); } catch (error) { console.error('Webhook error:', error); res.status(400).send('Bad request'); } }); ``` --- ## Environment-Specific Issues ### Local Development (ngrok) **Issue**: ngrok tunnel not accessible ```bash # ❌ Problem Error: ngrok tunnel not found # ✅ Solution ngrok http 3000 --log=stdout ``` **Issue**: ngrok free tier URL changes ```bash # ✅ Solution: Use ngrok authtoken for persistent URLs ngrok authtoken YOUR_TOKEN ngrok http 3000 --hostname=your-subdomain.ngrok-free.app ``` ### Staging/Production **Issue**: Firewall blocking Field Nation IPs **Solution**: Whitelist Field Nation IPs: **Sandbox:** ``` 18.215.51.196 3.223.100.250 44.199.193.222 ``` **Production:** Contact Field Nation support [Complete security guide →](/docs/webhooks/guides/security) --- ## Error Response Codes ### Issue Webhooks retrying when they shouldn't. ### Solution Return appropriate status codes: ```javascript app.post('/webhooks', async (req, res) => { try { // Signature verification failure if (!verifySignature(req)) { return res.status(401).send('Unauthorized'); // Won't retry } // Invalid payload format if (!validatePayload(req.body)) { return res.status(400).send('Bad request'); // Won't retry } // Process webhook await processEvent(req.body); // Success return res.status(200).send('OK'); } catch (error) { if (error.retriable) { // Temporary issue - allow retry return res.status(500).send('Internal error'); } else { // Permanent issue - don't retry return res.status(422).send('Unprocessable'); } } }); ``` | Status | Retry? | Use Case | |--------|--------|----------| | 200-299 | No | Success | | 400 | No | Invalid request | | 401 | No | Invalid signature | | 404 | No | Endpoint not found | | 410 | No | Endpoint gone | | 422 | No | Unprocessable | | 500-599 | Yes | Server error | | Timeout | Yes | Network issue | [Complete delivery mechanics →](/docs/webhooks/concepts/delivery) --- ## Quick Diagnostic Checklist When webhooks aren't working, check: - ☐ Webhook status is `active` - ☐ Events are subscribed - ☐ Endpoint is HTTPS (not HTTP) - ☐ Endpoint is publicly accessible - ☐ SSL certificate is valid - ☐ Signature verification is correct - ☐ Response time < 30 seconds - ☐ Correct status codes returned - ☐ Firewall allows Field Nation IPs - ☐ Delivery logs show no errors --- --- ### Debugging URL: /docs/webhooks/troubleshooting/debugging ## Debugging Tools ### Request Inspectors Perfect for viewing raw webhook payloads during development: | Tool | URL | Features | |------|-----|----------| | **webhook.site** | [webhook.site](https://webhook.site) | No signup, custom responses, request history | | **RequestBin** | [requestbin.com](https://requestbin.com) | Public/private bins, expiring URLs | | **ngrok Inspector** | `http://127.0.0.1:4040` | Built-in with ngrok, real-time requests | ### Local Debugging Server ```javascript title="debug-webhook-server.js" const express = require('express'); const crypto = require('crypto'); const fs = require('fs'); const app = express(); // Raw body for signature verification app.use(express.raw({ type: 'application/json' })); // Detailed logging middleware app.use((req, res, next) => { const logEntry = { timestamp: new Date().toISOString(), method: req.method, url: req.url, headers: req.headers, body: req.body ? req.body.toString() : null }; // Log to console console.log('\n' + '='.repeat(80)); console.log('WEBHOOK REQUEST'); console.log('='.repeat(80)); console.log(JSON.stringify(logEntry, null, 2)); // Log to file fs.appendFileSync('webhook-debug.log', JSON.stringify(logEntry) + '\n'); next(); }); // Webhook endpoint app.post('/webhooks/fieldnation', (req, res) => { try { // Extract headers const signature = req.headers['x-fn-signature']; const webhookId = req.headers['x-fn-webhook-id']; const eventName = req.headers['x-fn-event-name']; const deliveryId = req.headers['x-fn-delivery-id']; const timestamp = req.headers['x-fn-timestamp']; console.log('\n📋 WEBHOOK HEADERS:'); console.log(` Signature: ${signature}`); console.log(` Webhook ID: ${webhookId}`); console.log(` Event: ${eventName}`); console.log(` Delivery ID: ${deliveryId}`); console.log(` Timestamp: ${timestamp}`); // Verify signature const isValid = verifySignature( req.body, signature, process.env.WEBHOOK_SECRET ); console.log(`\n🔐 SIGNATURE VERIFICATION: ${isValid ? '✅ VALID' : '❌ INVALID'}`); if (!isValid) { console.log('\n❌ SIGNATURE VERIFICATION FAILED'); console.log('Expected secret:', process.env.WEBHOOK_SECRET); console.log('Received signature:', signature); return res.status(401).send('Unauthorized'); } // Parse payload const payload = JSON.parse(req.body.toString()); console.log('\n📦 PARSED PAYLOAD:'); console.log(JSON.stringify(payload, null, 2).substring(0, 500) + '...'); console.log('\n✅ WEBHOOK PROCESSED SUCCESSFULLY'); console.log('='.repeat(80) + '\n'); res.status(200).send('OK'); } catch (error) { console.error('\n❌ ERROR PROCESSING WEBHOOK:'); console.error(error); console.log('='.repeat(80) + '\n'); 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) ); } // Health check app.get('/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString(), secret: process.env.WEBHOOK_SECRET ? 'configured' : 'missing' }); }); const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(`🚀 Debug webhook server running on port ${PORT}`); console.log(`📝 Logs being written to: webhook-debug.log`); console.log(`🔐 Webhook secret: ${process.env.WEBHOOK_SECRET || 'NOT SET'}`); console.log(`\n⏳ Waiting for webhooks...\n`); }); ``` **Run:** ```bash export WEBHOOK_SECRET=your-webhook-secret node debug-webhook-server.js ``` --- ## Tracing Request Flow ### End-to-End Trace ```mermaid sequenceDiagram participant FN as Field Nation participant LB as Load Balancer participant Proxy as Reverse Proxy participant App as Your App participant DB as Database FN->>LB: POST /webhooks Note over FN,LB: Check: DNS resolution LB->>Proxy: Forward request Note over LB,Proxy: Check: SSL termination Proxy->>App: Proxy pass Note over Proxy,App: Check: Proxy headers App->>App: Verify signature Note over App: Check: Secret match App->>App: Parse JSON Note over App: Check: Valid JSON App->>DB: Check idempotency Note over App,DB: Check: Event ID exists App->>FN: 200 OK Note over App,FN: Response < 30s activate App App->>App: Process async deactivate App ``` ### Add Trace IDs ```javascript const { v4: uuidv4 } = require('uuid'); app.post('/webhooks/fieldnation', (req, res) => { const traceId = uuidv4(); const deliveryId = req.headers['x-fn-delivery-id']; console.log(`[${traceId}] Starting webhook processing`); console.log(`[${traceId}] Delivery ID: ${deliveryId}`); try { console.log(`[${traceId}] Verifying signature...`); verifySignature(req); console.log(`[${traceId}] Parsing payload...`); const payload = JSON.parse(req.body.toString()); console.log(`[${traceId}] Event: ${payload.eventName}`); console.log(`[${traceId}] Work Order: ${payload.workOrderId}`); console.log(`[${traceId}] Responding 200 OK`); res.status(200).send('OK'); console.log(`[${traceId}] Queuing for async processing`); queue.add({ ...payload, traceId }); } catch (error) { console.error(`[${traceId}] Error: ${error.message}`); res.status(500).send('Internal error'); } }); ``` --- ## Network Debugging ### Test with curl ```bash # Basic test curl -X POST https://your-endpoint.com/webhooks \ -H "Content-Type: application/json" \ -d '{"test": "data"}' \ -v # With signature SECRET="your-secret" PAYLOAD='{"test":"data"}' SIGNATURE=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}') curl -X POST https://your-endpoint.com/webhooks \ -H "Content-Type: application/json" \ -H "x-fn-signature: sha256=$SIGNATURE" \ -H "x-fn-webhook-id: wh_test" \ -H "x-fn-event-name: test.event" \ -H "x-fn-delivery-id: del_test" \ -d "$PAYLOAD" \ -v ``` ### Check SSL/TLS ```bash # Verify certificate openssl s_client -connect your-domain.com:443 -servername your-domain.com # Check certificate expiry echo | openssl s_client -servername your-domain.com \ -connect your-domain.com:443 2>/dev/null | \ openssl x509 -noout -dates # Test from specific IP (simulate Field Nation) curl -X POST https://your-endpoint.com/webhooks \ --resolve your-domain.com:443:YOUR_SERVER_IP \ -H "Content-Type: application/json" \ -d '{"test": "data"}' ``` ### Check DNS ```bash # DNS lookup dig your-domain.com # Check propagation nslookup your-domain.com # Trace route traceroute your-domain.com ``` --- ## Signature Verification Debugging ### Compare Hashes ```javascript function debugSignatureVerification(rawBody, signature, secret) { console.log('\n🔐 SIGNATURE VERIFICATION DEBUG'); console.log('='.repeat(50)); // Parse signature const [algorithm, providedHash] = signature.split('='); console.log(`Algorithm: ${algorithm}`); console.log(`Provided hash: ${providedHash}`); // Calculate expected hash const expectedHash = crypto .createHmac(algorithm, secret) .update(rawBody) .digest('hex'); console.log(`Expected hash: ${expectedHash}`); // Compare const match = expectedHash === providedHash; console.log(`\nMatch: ${match ? '✅ YES' : '❌ NO'}`); if (!match) { console.log('\n❌ MISMATCH DETAILS:'); console.log(`Secret used: ${secret}`); console.log(`Body length: ${rawBody.length} bytes`); console.log(`Body preview: ${rawBody.toString().substring(0, 100)}...`); // Try common issues console.log('\n🔍 TRYING COMMON ISSUES:'); // Issue 1: Body already parsed try { const parsedBody = JSON.parse(rawBody.toString()); const hashFromParsed = crypto .createHmac(algorithm, secret) .update(JSON.stringify(parsedBody)) .digest('hex'); console.log(`Hash from re-stringified JSON: ${hashFromParsed}`); console.log(` Match: ${hashFromParsed === providedHash ? '✅' : '❌'}`); } catch (e) {} // Issue 2: Different secret const testSecrets = [ secret.toUpperCase(), secret.toLowerCase(), secret.trim() ]; testSecrets.forEach(testSecret => { const testHash = crypto .createHmac(algorithm, testSecret) .update(rawBody) .digest('hex'); if (testHash === providedHash) { console.log(` ✅ MATCH with modified secret: "${testSecret}"`); } }); } console.log('='.repeat(50) + '\n'); return match; } ``` --- ## Payload Debugging ### Inspect Payload Structure ```javascript function debugPayload(payload) { console.log('\n📦 PAYLOAD DEBUGGING'); console.log('='.repeat(50)); // Type and size console.log(`Type: ${typeof payload}`); console.log(`Size: ${JSON.stringify(payload).length} bytes`); // Structure console.log('\nStructure:'); console.log(` eventId: ${payload.eventId}`); console.log(` eventName: ${payload.eventName}`); console.log(` workOrderId: ${payload.workOrderId}`); console.log(` timestamp: ${payload.timestamp}`); console.log(` data keys: ${Object.keys(payload.data || {}).join(', ')}`); // Validation console.log('\nValidation:'); console.log(` Has eventId: ${!!payload.eventId ? '✅' : '❌'}`); console.log(` Has eventName: ${!!payload.eventName ? '✅' : '❌'}`); console.log(` Has data: ${!!payload.data ? '✅' : '❌'}`); console.log(` Valid timestamp: ${isValidDate(payload.timestamp) ? '✅' : '❌'}`); // Content preview console.log('\nFull payload:'); console.log(JSON.stringify(payload, null, 2)); console.log('='.repeat(50) + '\n'); } function isValidDate(dateString) { const date = new Date(dateString); return date instanceof Date && !isNaN(date); } ``` --- ## Performance Debugging ### Measure Processing Time ```javascript async function processWebhookWithTiming(payload) { const timings = {}; const start = Date.now(); timings.start = start; // Signature verification let checkpoint = Date.now(); await verifySignature(payload); timings.signatureVerification = Date.now() - checkpoint; // Idempotency check checkpoint = Date.now(); const isDuplicate = await checkIdempotency(payload.eventId); timings.idempotencyCheck = Date.now() - checkpoint; if (isDuplicate) { timings.total = Date.now() - start; console.log('Timings:', timings); return; } // Parse and validate checkpoint = Date.now(); validatePayload(payload); timings.validation = Date.now() - checkpoint; // Process checkpoint = Date.now(); await handleEvent(payload); timings.processing = Date.now() - checkpoint; // Total timings.total = Date.now() - start; console.log('\n⏱️ PERFORMANCE TIMINGS:'); console.log(` Signature verification: ${timings.signatureVerification}ms`); console.log(` Idempotency check: ${timings.idempotencyCheck}ms`); console.log(` Validation: ${timings.validation}ms`); console.log(` Processing: ${timings.processing}ms`); console.log(` TOTAL: ${timings.total}ms`); if (timings.total > 5000) { console.warn('⚠️ WARNING: Processing took > 5 seconds'); } return timings; } ``` --- ## Common Debug Scenarios ### Scenario 1: Webhooks Work Locally, Fail in Production **Debug Steps:** ```bash # 1. Check environment variables echo $WEBHOOK_SECRET # 2. Test connectivity from production curl -v https://your-production-endpoint.com/health # 3. Check production logs tail -f /var/log/your-app/webhook.log # 4. Compare environments diff local.env production.env ``` ### Scenario 2: Intermittent Failures **Debug with Detailed Logging:** ```javascript const winston = require('winston'); const logger = winston.createLogger({ level: 'debug', format: winston.format.combine( winston.format.timestamp(), winston.format.json() ), transports: [ new winston.transports.File({ filename: 'webhook-error.log', level: 'error' }), new winston.transports.File({ filename: 'webhook-debug.log' }) ] }); app.post('/webhooks', async (req, res) => { const context = { deliveryId: req.headers['x-fn-delivery-id'], timestamp: new Date().toISOString(), ip: req.ip }; logger.debug('Webhook received', context); try { logger.debug('Verifying signature', context); await verifySignature(req); logger.debug('Parsing payload', context); const payload = JSON.parse(req.body.toString()); logger.debug('Processing event', { ...context, eventName: payload.eventName }); await processEvent(payload); logger.info('Webhook processed successfully', context); res.status(200).send('OK'); } catch (error) { logger.error('Webhook processing failed', { ...context, error: error.message, stack: error.stack }); res.status(500).send('Internal error'); } }); ``` --- --- ### Delivery failures URL: /docs/webhooks/troubleshooting/delivery-failures ## Analyzing Delivery Logs ### Access Delivery Logs ```bash curl -X GET "https://api-sandbox.fndev.net/api/v1/webhooks/delivery-logs?webhookId=wh_abc123&deliveryStatus=500" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ### Get Detailed Log ```bash curl -X GET https://api-sandbox.fndev.net/api/v1/webhooks/delivery-logs/del_xyz789 \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` Response includes pre-signed URL to full log file with request/response details. --- ## Common Delivery Errors ### Connection Refused (ECONNREFUSED) **Error Message:** ``` Error: connect ECONNREFUSED 192.168.1.100:443 ``` **Causes:** - Server not running - Wrong port - Firewall blocking connections **Solutions:** ### Verify Server is Running ```bash # Check if server is listening netstat -tulpn | grep :3000 # Or lsof -i :3000 ``` ### Test Local Connectivity ```bash curl -X POST http://localhost:3000/webhooks \ -H "Content-Type: application/json" \ -d '{"test": "data"}' ``` ### Check Firewall Rules ```bash # Allow incoming connections sudo ufw allow 3000/tcp # Or sudo iptables -A INPUT -p tcp --dport 3000 -j ACCEPT ``` ### Verify ngrok/Tunnel ```bash # Restart ngrok ngrok http 3000 # Use new URL in webhook configuration ``` --- ### SSL Certificate Error **Error Message:** ``` Error: self signed certificate Error: unable to verify the first certificate ``` **Causes:** - Self-signed certificate - Expired certificate - Certificate chain incomplete **Solutions:** ```bash # Install certbot sudo apt-get install certbot python3-certbot-nginx # Generate certificate sudo certbot --nginx -d your-domain.com # Auto-renew sudo certbot renew --dry-run ``` ```bash # Verify certificate openssl s_client -connect your-domain.com:443 -servername your-domain.com # Check expiry echo | openssl s_client -servername your-domain.com -connect your-domain.com:443 2>/dev/null | openssl x509 -noout -dates ``` For local testing only, use ngrok which provides valid HTTPS: ```bash ngrok http 3000 # Use provided HTTPS URL ``` ⚠️ Never use self-signed certificates in production --- ### Timeout (30 seconds) **Error Message:** ``` Error: Timeout of 30000ms exceeded ``` **Cause:** Endpoint takes too long to respond **Solution:** Implement async processing ```javascript // ❌ Wrong - slow synchronous processing app.post('/webhooks', async (req, res) => { const result = await slowOperation(); // 35 seconds res.status(200).send('OK'); // Timeout already occurred! }); // ✅ Correct - respond immediately const queue = new Queue('webhooks'); app.post('/webhooks', async (req, res) => { // Quick validation if (!verifySignature(req)) { return res.status(401).send('Unauthorized'); } const payload = JSON.parse(req.body.toString()); // Queue for processing await queue.add(payload); // Respond within 1 second res.status(200).send('OK'); }); // Worker processes queue queue.process(async (job) => { await slowOperation(job.data); // Can take as long as needed }); ``` --- ### DNS Resolution Failed **Error Message:** ``` Error: getaddrinfo ENOTFOUND your-domain.com ``` **Causes:** - Domain doesn't exist - DNS not propagated - DNS server issues **Solutions:** ```bash # Check DNS resolution dig your-domain.com # Check DNS propagation nslookup your-domain.com # Test from Field Nation perspective curl https://your-domain.com/webhooks ``` If domain is new, wait 24-48 hours for DNS propagation. --- ### 502 Bad Gateway **Error Message:** ``` HTTP 502 Bad Gateway ``` **Causes:** - Reverse proxy misconfiguration - Backend server down - Timeout at proxy level **Solutions:** ```nginx # /etc/nginx/sites-available/your-site server { listen 443 ssl; server_name your-domain.com; location /webhooks { proxy_pass http://localhost:3000; proxy_http_version 1.1; # Important timeout settings proxy_connect_timeout 30s; proxy_send_timeout 30s; proxy_read_timeout 30s; # Required headers proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } # Test configuration sudo nginx -t # Reload sudo systemctl reload nginx ``` ```apache # /etc/apache2/sites-available/your-site.conf ServerName your-domain.com ProxyPreserveHost On ProxyTimeout 30 ProxyPass /webhooks http://localhost:3000/webhooks ProxyPassReverse /webhooks http://localhost:3000/webhooks # SSL configuration SSLEngine on SSLCertificateFile /path/to/cert.pem SSLCertificateKeyFile /path/to/key.pem # Test configuration sudo apachectl configtest # Reload sudo systemctl reload apache2 ``` ```javascript // Ensure app is running and responsive const app = express(); app.post('/webhooks', (req, res) => { console.log('Webhook received'); res.status(200).send('OK'); }); const server = app.listen(3000, () => { console.log('Server running on port 3000'); }); // Health check endpoint app.get('/health', (req, res) => { res.status(200).send('OK'); }); // Graceful shutdown process.on('SIGTERM', () => { server.close(() => { console.log('Server closed'); }); }); ``` --- ### 500 Internal Server Error **Error Message:** ``` HTTP 500 Internal Server Error ``` **Cause:** Unhandled exception in your webhook handler **Solution:** Add comprehensive error handling ```javascript app.post('/webhooks', async (req, res) => { try { // Verify signature if (!verifySignature(req)) { return res.status(401).send('Unauthorized'); } // Parse payload const payload = JSON.parse(req.body.toString()); // Process webhook await processWebhook(payload); res.status(200).send('OK'); } catch (error) { console.error('Webhook processing error:', error); // Log error details await logError({ error: error.message, stack: error.stack, payload: req.body.toString(), headers: req.headers }); // Return 500 to trigger retry res.status(500).send('Internal error'); } }); // Global error handler app.use((error, req, res, next) => { console.error('Unhandled error:', error); res.status(500).send('Internal error'); }); ``` --- ## Retry Patterns ### Manual Retry ```bash curl -X PATCH https://api-sandbox.fndev.net/api/v1/webhooks/delivery-logs/del_xyz789/retry \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ### Bulk Retry Failed Deliveries ```javascript async function retryFailedDeliveries(webhookId, hours = 24) { // Get failed deliveries const response = await fetch( `https://api-sandbox.fndev.net/api/v1/webhooks/delivery-logs?` + `webhookId=${webhookId}&deliveryStatus=500`, { headers: { 'Authorization': `Bearer ${accessToken}` } } ); const { result: logs } = await response.json(); // Filter to recent failures const since = new Date(Date.now() - hours * 60 * 60 * 1000); const recentFailures = logs.filter(log => new Date(log.createdAt) >= since ); console.log(`Retrying ${recentFailures.length} failed deliveries...`); // Retry each for (const log of recentFailures) { try { await fetch( `https://api-sandbox.fndev.net/api/v1/webhooks/delivery-logs/${log.deliveryId}/retry`, { method: 'PATCH', headers: { 'Authorization': `Bearer ${accessToken}` } } ); console.log(`✅ Retried ${log.deliveryId}`); } catch (error) { console.error(`❌ Failed to retry ${log.deliveryId}:`, error.message); } } } // Usage await retryFailedDeliveries('wh_abc123', 24); ``` --- ## Monitoring Delivery Health ```javascript async function checkDeliveryHealth(webhookId) { const response = await fetch( `https://api-sandbox.fndev.net/api/v1/webhooks/delivery-logs?webhookId=${webhookId}&perPage=100`, { headers: { 'Authorization': `Bearer ${accessToken}` } } ); const { result: logs } = await response.json(); if (logs.length === 0) { return { status: 'no_data', message: 'No deliveries yet' }; } const successful = logs.filter(log => log.deliveryStatus >= 200 && log.deliveryStatus < 300 ).length; const successRate = (successful / logs.length) * 100; const health = { total: logs.length, successful, failed: logs.length - successful, successRate: successRate.toFixed(2) + '%', status: successRate > 95 ? 'healthy' : successRate > 80 ? 'degraded' : 'unhealthy' }; return health; } ``` --- --- ## Pre built Connectors (42) ### Getting started URL: /docs/connectors/getting-started ## Prerequisites ### Field Nation Requirements Company Profile).", required: true }, "Sandbox Access": { type: "Recommended", description: "Sandbox environment access for testing before production deployment.", required: false } }} /> [How to find your Company ID →](/docs/getting-started/prerequisites) --- ### External Platform Requirements You need administrative access in your external platform (Salesforce, ServiceNow, etc.) to: - Create API credentials or OAuth applications - Configure webhooks or outbound messages - Set up automation rules (Flows, Business Rules, etc.) - Grant API permissions to integration users Depending on the platform, you'll need: - **API Keys** - Unique identifiers for API access - **OAuth Tokens** - For OAuth 2.0 authenticated platforms - **Username & Password** - For Basic Authentication systems - **Security Tokens** - Platform-specific security credentials These are typically generated in the external platform's API or integration settings. Ensure your external platform can: - **Receive webhooks** from Field Nation's IP addresses - **Make outbound API calls** to Field Nation's endpoints - **Allow API access** (not blocked by firewalls or security policies) Some enterprise platforms require IP whitelisting or firewall rule adjustments. The integration user must have: - **Read access** to all fields you want to sync FROM the external system - **Write access** to all fields you want to update IN the external system - **API-enabled permissions** on custom objects and fields - **Create/Update permissions** for the object types being synchronized --- ## Common Setup Workflow All pre-built connectors follow a similar configuration pattern: ### Gather Credentials Collect required information from your external platform: - API credentials (keys, tokens, username/password) - Instance URLs or identifiers - Object names or IDs you want to integrate - Security tokens (if applicable) ### Configure in Field Nation 1. Navigate to Integration Settings in Field Nation 2. Select your connector from available integrations 3. Enter connection credentials 4. Specify the external object to integrate with 5. Configure optional settings (messaging, custom fields) 6. Test connection and save ### Configure External Platform 1. Set up webhook or outbound message 2. Configure automation rules (Flows, triggers, etc.) 3. Map Field Nation's trigger URL as the destination 4. Define conditions for when to send data to Field Nation 5. Test the trigger ### Map Fields 1. Field Nation automatically discovers available fields 2. Configure field mappings (source field → target field) 3. Set transformation actions (sync, static, date convert, etc.) 4. Add custom JSONNET transformations if needed 5. Save field mappings ### Test Integration 1. Create a test record in your external system 2. Trigger the automation (meet your Flow/rule conditions) 3. Verify Field Nation work order is created 4. Update the work order in Field Nation 5. Confirm updates sync back to external system ### Go Live 1. Monitor first few synchronizations 2. Review error logs for any issues 3. Adjust field mappings if needed 4. Document configuration for your team 5. Set up alerting for integration errors --- ## Integration Broker Access All pre-built connectors are configured through Field Nation's **Integration Broker** interface: **Sandbox**: [ui-sandbox.fndev.net/integrations](https://ui-sandbox.fndev.net/integrations) **Production**: [app.fieldnation.com/integrations](https://app.fieldnation.com/integrations) > [INFO] **First Time Setup**: If you don't see the Integrations section, contact [Field Nation Support](https://app.fieldnation.com/support-cases) to enable connector access for your account. --- ## Understanding Field Mappings Field mappings define how data translates between systems. Each mapping consists of: ### Mapping Components ### Common Transformation Actions **Sync** - Direct field-to-field copy with no transformation **Set Static** - Always use a specific value (e.g., always set priority to "High") **Date Convert** - Transform date formats between systems **Concat** - Combine multiple fields into one (e.g., FirstName + LastName → FullName) **Array Map** - Transform array or list data **Range Map** - Map value ranges (e.g., 1-3 → Low, 4-7 → Medium, 8-10 → High) **Custom JSONNET** - Write custom transformation logic for complex scenarios [Learn more about field mappings →](/docs/connectors/concepts/field-mappings) --- ## Data Flow Patterns ### Inbound: External System → Field Nation ```mermaid graph LR A[Record Created/Updated] --> B[Webhook Triggered] B --> C[Integration Broker] C --> D[Fetch Full Record] D --> E[Apply Field Mappings] E --> F[Create/Update Work Order] F --> G[Log Success] ``` **Trigger**: Record change in external system **Result**: Field Nation work order created or updated --- ### Outbound: Field Nation → External System ```mermaid graph LR A[Work Order Event] --> B[Integration Broker] B --> C[Apply Reverse Mappings] C --> D[Call External API] D --> E[Update Record] E --> F[Confirm Sync] ``` **Trigger**: Work order status change, assignment, completion, etc. **Result**: External system record updated --- ## Authentication Methods Different platforms use different authentication approaches: **Used by**: Freshdesk, some custom REST APIs **Configuration**: - Username - Password - Optional: Security Token **Format**: `Authorization: Basic base64(username:password)` **Used by**: Salesforce, ServiceNow (optional) **Configuration**: - Client ID - Client Secret - Authorization URL - Token URL - Scopes **Flow**: Authorization Code or Client Credentials **Used by**: Quickbase, Smartsheet **Configuration**: - API Key or Application Token - Optional: API Secret **Format**: Custom header (e.g., `X-API-Key: your-key`) **Used by**: NetSuite, Autotask **Configuration**: - Account ID - Integration Token - Consumer Key/Secret - Token Key/Secret **Format**: Platform-specific SOAP authentication --- ## Best Practices ### Before Configuration - ☐ Test in sandbox environment first - ☐ Document current workflow and requirements - ☐ Identify which fields need to sync - ☐ Plan for error handling and monitoring - ☐ Review external platform's API limits ### During Configuration - ☐ Use descriptive names for field mappings - ☐ Start with minimal field set, expand later - ☐ Test each mapping individually - ☐ Document transformation logic - ☐ Keep track of credentials securely ### After Configuration - ☐ Monitor first 24 hours closely - ☐ Set up error notifications - ☐ Create runbook for common issues - ☐ Train team on monitoring dashboards - ☐ Schedule regular health checks --- ## Common Pitfalls to Avoid **Problem**: Integration fails due to missing permissions **Solution**: Ensure integration user has full API access and permissions for all objects and fields being synchronized **Problem**: Data fails to sync due to type mismatches **Solution**: Use appropriate transformation actions (Date Convert, Range Map) to match field types between systems **Problem**: Work order creation fails due to missing required data **Solution**: Either map all required fields or set static default values for fields not available in source system **Problem**: No data flows to Field Nation **Solution**: Verify webhook configuration, check firewall rules, confirm trigger conditions are met **Problem**: Integration stops working after some time **Solution**: Refresh OAuth tokens, verify credentials haven't been reset, check for password policy changes --- ## Testing Your Configuration ### Create Test Scenarios 1. **Happy Path**: Standard work order creation and completion 2. **Required Fields**: Ensure all required fields are populated 3. **Optional Fields**: Verify optional fields sync correctly 4. **Status Changes**: Test bidirectional status synchronization 5. **Error Handling**: Intentionally trigger errors to verify error handling ### Test Checklist - ☐ Work order created in Field Nation from external record - ☐ All mapped fields populated correctly - ☐ Status updates sync bidirectionally - ☐ Comments/messages sync (if enabled) - ☐ Error notifications received when sync fails - ☐ Retry logic works for temporary failures - ☐ Duplicate prevention works (same record not created twice) --- ## Getting Help ### Connector-Specific Guides Each platform has detailed configuration instructions: Flows & Outbound Messages setup Business Rules & REST Messages OpenAPI spec configuration [View all platform guides →](/docs/connectors/introduction) ### Support Resources - **Support Cases**: [app.fieldnation.com/support-cases](https://app.fieldnation.com/support-cases) - **Phone**: +1 877-573-4353 (24/7) - **Troubleshooting**: [Common issues and solutions](/docs/connectors/concepts/troubleshooting) --- --- ### Introduction URL: /docs/connectors/introduction ## What Are Pre-built Connectors? Pre-built connectors are **configuration-driven integrations** managed by Field Nation's Integration Broker middleware. Instead of building custom API integrations, you configure field mappings and business rules through a web interface. Field Nation handles authentication, data transformation, retries, and error recovery automatically. ### Key Benefits Configure through UI - no API development needed for standard workflows Updates, security patches, and API changes handled automatically Automatic retries, dead letter queues, and detailed error logging Work orders flow both ways - create in either system, sync status automatically --- ## Supported Platforms Field Nation provides pre-built connectors for 9 platforms: ### Professional Services Automation **Autotask** - Autotask PSA integration for ticket and work order synchronization **ConnectWise** - ConnectWise Manage integration for service ticket management **ServiceNow** - ServiceNow ITSM for incident and work order management [View all PSA connectors →](#platform-guides) ### CRM & Support **Salesforce** - Salesforce Service Cloud integration with Flows and Outbound Messages **Freshdesk** - Freshdesk ticketing system integration [View all CRM connectors →](#platform-guides) ### ERP & Project Management **NetSuite** - NetSuite ERP integration for work order and service management **Quickbase** - Bidirectional sync between Quickbase applications and Field Nation **Smartsheet** - Smartsheet project management data synchronization [View all platforms →](#platform-guides) ### Universal Integration **REST Connector** - Connect **any system** with an OpenAPI specification - Upload your OpenAPI spec file - Configure endpoints (create, read, update) - Map fields through UI - Basic authentication support Perfect for systems not covered by out-of-the-box connectors. [Learn more about REST Connector →](/docs/connectors/platforms/rest-connector/overview) --- ## How Pre-built Connectors Work ```mermaid sequenceDiagram participant Ext as External System participant Broker as Integration Broker participant FN as Field Nation Note over Ext,FN: Inbound: External System → Field Nation Ext->>Broker: Webhook/API Trigger Broker->>Ext: Fetch Full Record Data Broker->>Broker: Apply Field Mappings Broker->>Broker: Transform with JSONNET Broker->>FN: Create/Update Work Order Note over Ext,FN: Outbound: Field Nation → External System FN->>Broker: Work Order Event Broker->>Broker: Apply Reverse Mappings Broker->>Ext: Update Record via API Ext-->>Broker: Confirmation ``` ### Configuration in Field Nation Configure authentication credentials, specify the external object to integrate, and set up field mappings through the Integration Broker UI. ### External Platform Setup Configure webhooks or triggers in your external system to notify Field Nation when records are created or updated. ### Automatic Synchronization The Integration Broker handles bidirectional sync: - Creates Field Nation work orders from external records - Syncs status changes, assignments, and completions back to external system - Maintains data consistency with automatic retries --- ## When to Use Pre-built Connectors - Your platform is supported (Salesforce, ServiceNow, etc.) - You need standard work order synchronization - You want minimal development effort - You prefer configuration over coding - You need Field Nation to manage updates and maintenance - You need highly custom workflows - Your platform isn't supported by pre-built connectors - You require advanced data transformations beyond JSONNET - You want complete control over integration logic - You have development resources available [REST API + Webhooks Guide →](/docs/getting-started/quick-start) - Your system has an OpenAPI specification - You need basic CRUD operations - You want configuration-driven approach - Your system isn't in the pre-built connector list [REST Connector Guide →](/docs/connectors/platforms/rest-connector/overview) --- ## Common Use Cases ### Dispatch from CRM Automatically create Field Nation work orders when a Case, Ticket, or Opportunity reaches a specific status (e.g., "On-site Required"). ### Status Synchronization Reflect work order status changes (assigned, in progress, completed) back to your originating system in real-time. ### Provider Communication Sync comments and messages between Field Nation technicians and your internal team through the external platform. ### Financial Tracking Update work order costs, invoices, and payment status in your ERP or accounting system automatically. --- ## Feature Comparison | Feature | Pre-built Connectors | REST API + Webhooks | |---------|---------------------|---------------------| | **Setup Time** | Hours to days | Days to weeks | | **Development Required** | Configuration only | Full development | | **Customization** | High (via JSONNET) | Unlimited | | **Maintenance** | Field Nation | Your team | | **Cost** | Lower | Higher (dev time) | | **API Updates** | Handled automatically | Manual updates | | **Error Handling** | Built-in | Custom implementation | | **Best For** | Standard workflows | Custom workflows | [Compare all approaches →](/docs/getting-started/quick-start) --- ## Architecture Overview Pre-built connectors use Field Nation's **Integration Broker** - a microservices-based middleware that: - **Authenticates** with external platforms (OAuth, API keys, Basic Auth) - **Discovers Fields** automatically from external system metadata - **Transforms Data** using configurable field mappings and JSONNET - **Queues Messages** with Redis for reliable delivery and retries - **Handles Errors** with automatic retries, dead letter queues, and alerting [Learn more about broker architecture →](/docs/connectors/concepts/broker-architecture) --- ## Getting Started What you need before setting up a connector Select from 9 supported platforms Learn how to map fields and transform data Common issues and solutions --- ## Platform Guides Select your platform to get started: Integrate with Salesforce Service Cloud Connect ServiceNow ITSM Sync with Autotask PSA Integrate ConnectWise Manage Connect Freshdesk ticketing Integrate NetSuite ERP Sync Quickbase applications Connect Smartsheet projects Universal OpenAPI-based integration --- ## Support & Resources - **Support Portal**: [app.fieldnation.com/support-cases](https://app.fieldnation.com/support-cases) - **Phone Support**: +1 877-573-4353 (24/7) - **Status Page**: [status.fieldnation.com](https://status.fieldnation.com) [Complete support resources →](/docs/resources/support) --- --- ### Broker architecture URL: /docs/connectors/concepts/broker-architecture ## System Overview ```mermaid graph TB subgraph "External Systems" SF[Salesforce] SN[ServiceNow] AT[Autotask] OT[Other Platforms] end subgraph "Integration Broker" TRG[Trigger Handler] Q[Message Queues] WRK[Broker Workers] MAP[Field Mapping Engine] JSNT[JSONNET Processor] end subgraph "Field Nation" API[REST API] EVT[Event Stream] WO[Work Orders] end SF --> TRG SN --> TRG AT --> TRG OT --> TRG TRG --> Q Q --> WRK WRK --> MAP MAP --> JSNT JSNT --> API API --> WO EVT --> Q WRK --> SF WRK --> SN WRK --> AT WRK --> OT ``` --- ## Core Components ### Broker Workers The primary processing engine that: - Consumes messages from Redis queues - Executes field mappings and transformations - Dispatches API calls to target systems - Handles retries and error recovery ### Message Queues Redis-based queue system providing: - **Inbound Queue**: Webhook triggers from external systems - **Processing Queue**: Active operations being executed - **Retry Queue**: Failed operations with exponential backoff - **Dead Letter Queue (DLQ)**: Operations exceeding max retries ### Field Mapping Engine Dynamic mapping system that: - Translates data schemas between systems - Applies transformation actions (sync, static, date convert, etc.) - Supports conditional logic and complex mappings - Handles bidirectional field mapping ### Custom Action Processor JSONNET-based transformation engine for: - Complex data manipulation logic - Conditional field population - Custom business rules - Advanced array and object transformations [Learn more about JSONNET →](/docs/connectors/concepts/custom-actions) --- ## Data Flow ### Inbound: External System → Field Nation ### Trigger Reception External system sends webhook notification when record is created/updated. Each connector has a unique trigger URL with embedded authentication token. ### Queue Processing Broker worker retrieves message, validates token, fetches complete record data from external system using configured API credentials. ### Field Mapping System applies configured mappings to transform external data into Field Nation work order schema. Includes static values, date conversions, JSONNET transformations. ### Work Order Creation Transformed data is validated and submitted to Field Nation API. System stores correlation between external record ID and Field Nation work order ID. ### Error Handling Failed operations are logged, retried with exponential backoff, and moved to DLQ if max retries exceeded. Email notifications sent for persistent failures. --- ### Outbound: Field Nation → External System ### Event Detection Field Nation publishes work order lifecycle events (status changes, assignments, completions, messages) to integration event stream. ### Mapping Resolution Broker identifies originating external system using correlation ID. Applies reverse field mappings to transform Field Nation data to external schema. ### API Dispatch System constructs API request (REST, SOAP, or platform SDK) and submits update to external platform using configured authentication. ### Sync Confirmation Successful updates logged, work order sync status updated. Failed updates trigger retry with exponential backoff. --- ## Message Processing ### Queue Processing Strategy The broker uses a **poll-and-acknowledge pattern**: 1. Worker retrieves message from queue 2. Worker processes message completely 3. Worker acknowledges successful processing 4. If worker crashes, message remains unacknowledged and retries after timeout ### Rate Limiting Each integration respects external API quotas: - Maximum requests per minute - Concurrent connection limits - Burst allowances - Per-platform rate limit configuration ### Retry Logic Failed operations retry with exponential backoff: | Attempt | Delay | Action | |---------|-------|--------| | 1 | 0s | Immediate | | 2 | 30s | First retry | | 3 | 60s | Second retry | | 4 | 120s | Third retry | | 5 | 240s | Fourth retry | | 6+ | 480s | Final attempts | After maximum retries, operation moves to Dead Letter Queue for manual review. --- ## Authentication & Security ### Credential Management - **Encryption at Rest**: AES-256 encryption for all credentials - **In-Memory Only**: Decrypted only during active use - **No Logging**: Credentials never logged or exposed in errors - **Token Refresh**: Automatic OAuth token refresh ### Authentication Methods Supported ``` Authorization: Basic base64(username:password) ``` Used by: Freshdesk, REST Connector ``` Authorization: Bearer {access_token} ``` Automatic token refresh with refresh tokens Used by: Salesforce, ServiceNow ``` X-API-Key: {api_key} Authorization: Bearer {token} ``` Platform-specific header formats Used by: Quickbase, Smartsheet ```xml {account_id} {consumer_key} {token} ``` Used by: NetSuite, Autotask ### Request Authentication Each trigger URL includes unique client token: ``` https://api.fieldnation.com/integrations/trigger/{client_token} ``` Broker validates token before processing any data. --- ## Error Handling ### Error Categories **Authentication Errors** - Invalid/expired credentials - Insufficient permissions - Token refresh failures - **Action**: Manual intervention required **Validation Errors** - Missing required fields - Invalid data types - Schema validation failures - **Action**: Fix field mappings or source data **API Errors** - Rate limit exceeded - Network timeouts - Service unavailable - **Action**: Automatic retry with backoff **Transformation Errors** - JSONNET execution failures - Field mapping exceptions - Data type conversions - **Action**: Review custom actions ### Error Notifications Configure email alerts for: - Failed operations - Dead letter queue entries - Authentication failures - High error rates --- ## Performance Characteristics ### Processing Capacity - **Throughput**: Thousands of work orders per minute - **Scaling**: Horizontal scaling via additional worker instances - **Concurrency**: Multiple workers process queue in parallel ### Latency - **Typical**: 2-5 seconds end-to-end (trigger → work order created) - **Factors**: External API response time, field mapping complexity, queue depth ### Reliability - **Uptime**: 99.9% availability - **Failover**: Automatic worker recovery - **Circuit Breakers**: Isolate failing external systems - **Health Checks**: Continuous monitoring --- ## Monitoring & Observability ### Integration Logs All operations logged with: - Timestamps - User identifiers - Operation types - Outcomes (success/failure) - Error details - API request/response ### Health Dashboards Track key metrics: - Queue depths by type - Processing rates - Error frequencies by category - API response times - Sync success rates ### Audit Trail Comprehensive audit records for: - Compliance requirements - Incident investigation - Performance analysis - Capacity planning --- ## Configuration ### Field Nation Settings **Integration Broker UI**: [app.fieldnation.com/integrations](https://app.fieldnation.com/integrations) Configure: - Authentication credentials (encrypted) - Field mappings (source → target) - Event triggers (which events sync) - Operation types (create only vs bidirectional) - Email notifications ### External Platform Setup **Required Configuration**: - Webhook endpoints pointing to Field Nation trigger URL - Automation rules (Flows, Business Rules, Workflows) - API user with appropriate permissions - Firewall/IP whitelist configuration (if applicable) [Platform-specific guides →](/docs/connectors/introduction) --- ## Data Privacy ### Data Handling - **In-Transit Processing**: Data processed in memory, not stored - **Logging**: Error logs scrubbed of sensitive information - **Retention**: Audit logs retained per compliance requirements - **Encryption**: All API calls use TLS 1.2+ ### Compliance The Integration Broker supports: - SOC 2 compliance - GDPR data handling - Audit trail requirements - Data residency considerations --- ## Best Practices ### Configuration - ✅ Test in sandbox before production - ✅ Use service accounts (not personal credentials) - ✅ Set up error notifications - ✅ Monitor queue depths - ✅ Document field mappings ### Security - ✅ Rotate credentials periodically - ✅ Use least-privilege API access - ✅ Enable IP whitelisting where possible - ✅ Review audit logs regularly - ✅ Encrypt credentials in your documentation ### Performance - ✅ Keep field mappings simple when possible - ✅ Use custom actions only when necessary - ✅ Batch updates where supported - ✅ Monitor API rate limits - ✅ Scale workers for high volume --- ## Troubleshooting ### Common Issues **Check:** - Webhook triggering correctly - Authentication credentials valid - All required fields mapped - Queue processing (not stuck) - External API accessible **Check:** - Event triggers configured - Outbound field mappings correct - External API permissions - Correlation ID stored correctly - Rate limits not exceeded **Check:** - External API status - Credential expiration - Field mapping issues - Data validation errors - Network connectivity [Complete troubleshooting guide →](/docs/connectors/concepts/troubleshooting) --- --- ### Connectors vs api URL: /docs/connectors/concepts/connectors-vs-api Pre-built connectors and the REST API solve the same problem — moving data between Field Nation and your system. They differ in how much control you get, how much code you write, and how real-time events are handled. ## At a Glance | | Pre-built Connector | REST Connector | REST API + Webhooks | |---|---|---|---| | **You write code** | No | No | Yes | | **Setup** | Configure through UI | Upload OpenAPI spec, configure through UI | Build and deploy custom code | | **Field mapping** | Visual mapper | Visual mapper | Programmatic (you control the transform) | | **Bi-directional sync** | Built-in | Built-in | You implement both directions | | **Real-time events** | Built-in event triggers | Built-in event triggers | 33 webhook event types, you build the handler | | **Maintained by** | Field Nation | Field Nation | Your team | | **Granular control** | Limited to available mappings | Limited to available mappings | Complete | --- ## When to Use a Pre-built Connector A pre-built connector is the right starting point when: - **Your platform is supported** — Salesforce, ServiceNow, Autotask, ConnectWise, NetSuite, Freshdesk, Smartsheet, or QuickBase - **Standard sync is sufficient** — work order creation, status updates, field mapping between systems - **You want minimal maintenance** — Field Nation handles updates when the platform API changes - **Your team is not developer-heavy** — configuration happens through a UI, not code ### What connectors handle well Create and update work orders flowing both directions between your system and Field Nation. Keep status in sync automatically — when a work order moves to "Work Done" in Field Nation, your system reflects it. Built-in event handling fires sync on work order creation, status change, completion, and more. No separate webhook setup needed. Map standard fields (title, description, location, schedule, contacts) through a visual UI with transformation support. --- ## When to Use the REST Connector The REST Connector is the right choice when: - **Your system is not in the supported list** but exposes a REST API with an OpenAPI specification - **You want the same low-code experience** as a pre-built connector - **Standard field mapping covers your needs** - **You need event-driven sync** without writing webhook handlers The REST Connector auto-discovers your system's endpoints from the OpenAPI spec and lets you map fields through the same visual UI. It includes the same built-in event triggers as pre-built connectors. ### How it works ### Upload OpenAPI spec Provide your system's OpenAPI 3.x specification. The connector parses available endpoints, schemas, and auth methods. ### Map fields Use the visual mapper to connect Field Nation work order fields to your system's fields. Configure inbound and outbound sync. ### Enable events Choose which work order events trigger sync. The event layer is built-in — no webhook endpoint needed on your side. [Learn more about the REST Connector](/docs/connectors/platforms/rest-connector/overview) --- ## When to Use the REST API + Webhooks The REST API is the right choice when you need capabilities beyond what connector field mappings provide. Pair it with Webhooks for real-time event notifications. ### What the REST API gives you that connectors do not - **Bulk operations** — create or update hundreds of work orders in a single script - **Advanced querying** — filter work orders by any combination of fields, date ranges, status - **Custom reporting** — pull exactly the data you need in the shape you need it - **Full attachment control** — upload, organize, and manage documents and signatures - **Conditional routing** — route work orders to providers based on custom business rules - **Approval workflows** — programmatic approval chains based on your organization's logic - **SLA-based escalation** — trigger actions when deadlines approach - **Smart dispatch** — leverage Field Nation's dispatch algorithm via API - **Pay management** — set and update pay terms programmatically - **Expense tracking** — manage provider expenses, bonuses, and penalties - **Discount and increase management** — apply financial adjustments via API - **Bundle operations** — group work orders for consolidated invoicing ### Where webhooks complete the picture Without webhooks, your REST API integration must poll for changes. With webhooks, Field Nation pushes events to you instantly. | Scenario | Polling (REST API only) | Event-driven (REST API + Webhooks) | |---|---|---| | Detect work order status change | Poll every N minutes, compare state | Receive HTTP POST within seconds | | Know when a provider checks in | Poll check-in endpoint repeatedly | `work_order.checked_in` event fires | | Track task completion | Poll tasks endpoint per work order | `work_order.task.completed` event fires | | Monitor SLA approach | Calculate from schedule on each poll | `work_order.eta.updated` event fires | Field Nation supports [33 webhook event types](/docs/webhooks/concepts/events) covering the full work order lifecycle. --- ## Real-World Integration Patterns **Starting point:** Salesforce pre-built connector syncs work orders and status bi-directionally. **Problem:** The connector maps standard fields, but the buyer needs a weekly report aggregating work order costs by region — something field mappings cannot produce. **Solution:** REST API script runs on a schedule, queries `/workorders` with date and status filters, aggregates financial data, and pushes a summary back to Salesforce. **Event handling:** The connector's built-in events handle real-time sync. The reporting script runs independently. **Starting point:** ServiceNow connector creates Field Nation work orders from ITSM incidents. **Problem:** The buyer needs conditional routing logic — assign to different provider pools based on work order type and geography. Connector mappings handle field sync but not routing decisions. **Solution:** Connector still handles the baseline creation and field sync. A webhook listener receives `work_order.created` events and calls the REST API to apply custom routing logic before the work order is published. **Hybrid pattern:** Connector for data sync + Webhook + REST API for workflow control. **Starting point:** No connector exists for the buyer's proprietary workforce management system. **Solution:** REST API handles all CRUD operations. Webhook endpoint receives 6 critical events: `work_order.created`, `work_order.routed`, `work_order.work_done`, `work_order.approved`, `work_order.checked_in`, `work_order.checked_out`. Each event triggers a state update in the internal system. **Key design decisions:** Idempotent handlers using `eventId`, HMAC-SHA256 signature verification, exponential backoff on failures. --- ## Decision Summary ```mermaid flowchart LR A["Does a pre-built\nconnector exist?"] -->|Yes| B["Start with the Connector\n(events built-in)"] A -->|No| C["Does your system have\nan OpenAPI spec?"] C -->|Yes| D["Try the REST Connector\n(events built-in)"] C -->|No| E["Use REST API\n+ Webhooks"] B --> F{"Need more control?"} D --> F F -->|Yes| G["Add REST API calls\n+ Webhooks for events"] F -->|No| H["Done"] ``` > [INFO] **These approaches are complementary** You do not need to choose one path permanently. Many buyers start with a connector for fast time-to-value, then add targeted REST API calls and webhook listeners as their integration matures. --- ## Next Steps --- ### Custom actions URL: /docs/connectors/concepts/custom-actions ## JSONNET Overview JSONNET is a data templating language developed by Google that generates JSON through programmable logic. It's designed for configuration management and data transformation. Define reusable transformation logic with parameters Store intermediate values using `local` bindings Implement branching with `if-then-else` Built-in functions for strings, arrays, objects, math Include external libraries for code reuse > [INFO] **Learn More** For complete language reference, visit the [Official JSONNET Documentation →](https://jsonnet.org/) --- ## Execution Context Every custom action executes within a special context providing access to data and utilities: --- ## Basic Custom Action The simplest custom action directly accesses input fields: ```jsonnet { full_name: $.util.lookup_field($.input, 'assignee.user.first_name', '') + ' ' + $.util.lookup_field($.input, 'assignee.user.last_name', '') } ``` **Result**: Concatenates first and last name with space, using safe defaults for missing fields. --- ## Utility Library ($.util) The Integration Broker provides comprehensive utility functions optimized for field mappings. ### Field Lookup Functions Safely retrieves nested values using dot notation. ```jsonnet { work_order_id: $.util.lookup_field($.input, 'workorder.id', 0), company_name: $.util.lookup_field($.input, 'workorder.location.company.name', 'Unknown') } ``` Retrieves Field Nation custom field values by ID. ```jsonnet { po_number: $.util.lookup_custom_field('835', $.input, 'N/A'), site_contact: $.util.lookup_custom_field('912', $.input, '') } ``` > [INFO] Field Nation custom fields have nested structure - this function handles it automatically. --- ### Mapping Functions Maps values using exact match comparisons. ```jsonnet { payment_type: $.util.array_map( [ { cmp: "fixed", val: "Fixed Price" }, { cmp: "hourly", val: "Hourly Rate" }, { cmp: "blended", val: "Blended Rate" } ], $.util.lookup_field($.input, 'pay.type', ''), "Unknown" ) } ``` Maps numeric values to categories using range comparisons (right-to-left, less-than-or-equal). ```jsonnet { duration_category: $.util.range_map( [ { cmp: 0.5, val: "15-30 Minutes" }, { cmp: 1, val: "31-60 Minutes" }, { cmp: 2, val: "1-2 Hours" } ], $.util.lookup_field($.input, 'time_logs.hours', 0), "2+ Hours" ) } ``` **How it works:** - `0.3` hours → "15-30 Minutes" (≤ 0.5) - `1.5` hours → "1-2 Hours" (≤ 2) - `3` hours → "2+ Hours" (default) --- ### Date Conversion Functions Transforms dates between formats and timezones. ```jsonnet { scheduled_date: $.util.date_convert( $.util.lookup_field($.input, 'schedule.service_window.start.utc', ''), $.util.date_time_format_fn_utc(), // "YYYY-MM-DD HH:mm:ss" "MM/DD/YYYY h:mm:ss A", 'UTC', 'America/New_York' ) } ``` **Common Formats:** | Format | Description | | :--- | :--- | | `YYYY-MM-DD HH:mm:ss` | Standard datetime | | `MM/DD/YYYY` | US date format | | `h:mm A` | 12-hour time | | `X` | Unix timestamp (seconds) | | `x` | Unix timestamp (milliseconds) | Returns standard UTC format string: `"YYYY-MM-DD HH:mm:ss"` Adds one day to a date value. ```jsonnet { due_date: $.util.date_add( $.util.lookup_field($.input, 'schedule.service_window.start.utc', ''), $.util.date_time_format_fn_utc() ) } ``` --- ### Array Processing Functions Sums all numeric elements. ```jsonnet { total_hours: $.util.array_sum([10, 22, 4]) // Returns 36 } ``` Transforms array of objects by mapping their fields. ```jsonnet { mapped_expenses: $.util.object_map_array( $.input.pay.expense.results, { "amount": "expense_amount", "category.name": "expense_category", "description": "expense_desc" } ) } ``` ```json [ { "amount": 50, "category": { "name": "Travel" }, "description": "Mileage" }, { "amount": 25, "category": { "name": "Materials" }, "description": "Parts" } ] ``` ```json [ { "expense_amount": 50, "expense_category": "Travel", "expense_desc": "Mileage" }, { "expense_amount": 25, "expense_category": "Materials", "expense_desc": "Parts" } ] ``` Extracts values for a specific field from array of objects. ```jsonnet { all_categories: $.util.results_values( $.input.pay.expense.results, "category.name", 'Unknown' ) // Returns: ["Travel", "Materials", "Travel", "Lodging"] } ``` Like `results_values` but returns only unique values. ```jsonnet { unique_categories: $.util.results_values_unique( $.input.pay.expense.results, "category.name" ) // Returns: ["Travel", "Materials", "Lodging"] } ``` Groups array elements by field value with optional filtering. ```jsonnet { expenses_by_category: $.util.results_group_by( $.input.pay.expense.results, "category.name", $.util.is_expense_approved_fn, false ) } ``` --- ### Expense Aggregation Functions Filter function for approved expenses. ```jsonnet local approved_expenses = std.filter( $.util.is_expense_approved_fn, $.input.pay.expense.results ); ``` Sums a specific field across expense array. ```jsonnet { total_expense_amount: $.util.sum_expense_field( $.input.pay.expense.results, "amount" ) } ``` --- ## Common Patterns Use `if-then-else` with local variables for complex decisions: ```jsonnet { priority: local hours = $.util.lookup_field($.input, 'time_logs.hours', 0); local is_urgent = $.util.lookup_field($.input, 'priority', '') == 'urgent'; if is_urgent && hours > 2 then "Critical" else if is_urgent then "High" else if hours > 4 then "Medium" else "Low" } ``` Common string operations using the standard library: ```jsonnet { // Uppercase title_upper: std.asciiUpper($.util.lookup_field($.input, 'workorder.title', '')), // Extract email domain email_domain: local email = $.util.lookup_field($.input, 'user.email', ''); local parts = std.split(email, '@'); if std.length(parts) > 1 then parts[1] else '', // Remove whitespace clean_text: std.stripChars($.util.lookup_field($.input, 'description', ''), ' \t\n') } ``` Filter, map, and find operations: ```jsonnet { // Filter array high_value_expenses: local all_expenses = $.input.pay.expense.results; std.filter(function(e) e.amount > 100, all_expenses), // Map array expense_amounts: std.map( function(e) e.amount, $.input.pay.expense.results ), // Find element first_travel_expense: local expenses = $.input.pay.expense.results; local travel = std.filter( function(e) e.category.name == 'Travel', expenses ); if std.length(travel) > 0 then travel[0] else null } ``` Round, format, and calculate percentages: ```jsonnet { // Round to 2 decimal places rounded_cost: std.round($.input.pay.amount * 100) / 100, // Format as currency formatted_amount: local amount = $.util.lookup_field($.input, 'pay.amount', 0); "$" + std.toString(std.round(amount * 100) / 100), // Percentage calculation completion_rate: local completed = $.util.lookup_field($.input, 'tasks_completed', 0); local total = $.util.lookup_field($.input, 'tasks_total', 1); std.round((completed / total) * 100) + "%" } ``` Build nested objects from flat data: ```jsonnet { // Build nested object contact_info: { name: $.util.lookup_field($.input, 'assignee.user.first_name', '') + ' ' + $.util.lookup_field($.input, 'assignee.user.last_name', ''), email: $.util.lookup_field($.input, 'assignee.user.email', ''), phone: $.util.lookup_field($.input, 'assignee.user.phone', ''), address: { street: $.util.lookup_field($.input, 'location.address1', ''), city: $.util.lookup_field($.input, 'location.city', ''), state: $.util.lookup_field($.input, 'location.state', ''), zip: $.util.lookup_field($.input, 'location.zip', '') } } } ``` --- ## Advanced Examples Multi-condition status determination: ```jsonnet { external_status: local fn_status = $.util.lookup_field($.input, 'status.name', ''); local is_assigned = $.util.lookup_field($.input, 'assignee.user.id', 0) > 0; local is_completed = fn_status == 'work_done'; local is_approved = fn_status == 'approved'; if is_approved then "Closed - Approved" else if is_completed then "Closed - Completed" else if is_assigned then "In Progress" else if fn_status == 'published' then "Open - Available" else "Draft" } ``` Validate multiple fields and collect errors: ```jsonnet { validation_status: local title = $.util.lookup_field($.input, 'workorder.title', ''); local amount = $.util.lookup_field($.input, 'pay.amount', 0); local start_date = $.util.lookup_field($.input, 'schedule.service_window.start.utc', ''); local errors = []; local errors2 = if title == '' then errors + ["Missing title"] else errors; local errors3 = if amount <= 0 then errors2 + ["Invalid amount"] else errors2; local errors4 = if start_date == '' then errors3 + ["Missing schedule"] else errors3; if std.length(errors4) > 0 then { valid: false, errors: errors4 } else { valid: true, errors: [] } } ``` Select fields based on data conditions: ```jsonnet { primary_contact: local contacts = $.util.lookup_field($.input, 'workorder.contacts', []); local primary = std.filter( function(c) c.is_primary == true, contacts ); if std.length(primary) > 0 then primary[0].email else if std.length(contacts) > 0 then contacts[0].email else '' } ``` --- ## Best Practices ### Use Local Variables Store intermediate values for readability and performance: ```jsonnet { result: // Reusable, clear local hours = $.util.lookup_field($.input, 'time_logs.hours', 0); local rate = $.util.lookup_field($.input, 'pay.hourly_rate', 0); local total = hours * rate; std.round(total * 100) / 100 } ``` ```jsonnet { result: // Repeated lookups, hard to read std.round(($.util.lookup_field($.input, 'time_logs.hours', 0) * $.util.lookup_field($.input, 'pay.hourly_rate', 0)) * 100) / 100 } ``` ### Always Provide Defaults Prevent null pointer errors with safe defaults: ```jsonnet { // Safe with defaults total: $.util.lookup_field($.input, 'amount', 0) + $.util.lookup_field($.input, 'tax', 0) } ``` ```jsonnet { // May crash if fields missing total: $.input.amount + $.input.tax } ``` ### Cache Expensive Operations Calculate once, use multiple times: ```jsonnet { result: local all_expenses = $.input.pay.expense.results; local total = $.util.sum_expense_field(all_expenses, "amount"); local count = std.length(all_expenses); { total: total, count: count, average: if count > 0 then total / count else 0 } } ``` ### Check Types Before Operations Validate types to prevent runtime errors: ```jsonnet { safe_division: local numerator = $.util.lookup_field($.input, 'completed', 0); local denominator = $.util.lookup_field($.input, 'total', 1); if std.isNumber(numerator) && std.isNumber(denominator) && denominator != 0 then numerator / denominator else 0 } ``` --- ## Testing Custom Actions ### Test in Isolation Create a test payload and validate output: ```jsonnet // Test payload local test_input = { workorder: { id: 12345, title: "Test WO", pay: { amount: 150.50 } } }; // Your transformation local result = { id: test_input.workorder.id, formatted_amount: "$" + std.toString(test_input.workorder.pay.amount) }; // Output for testing result ``` ### Debug with std.trace Add trace statements to inspect values (output appears in integration logs): ```jsonnet { result: local value = $.util.lookup_field($.input, 'some.field', 'default'); local traced = std.trace("Field value: " + std.toString(value), value); // Transformation continues with traced value traced } ``` --- ## Troubleshooting > **Common Issues** - Missing commas between object properties - Unmatched brackets/braces - Missing semicolons in local bindings **Solution:** Use a JSONNET validator or linter before deploying. **Cause:** Accessing fields that don't exist ```jsonnet $.util.lookup_field($.input, 'field', 'default') ``` ```jsonnet $.input.field ``` **Cause:** Operations on mismatched types (string + number) **Solution:** Use type checking and conversion: ```jsonnet local value = $.util.lookup_field($.input, 'field', ''); if std.isNumber(value) then value else std.parseInt(value) ``` --- ### Events and sync URL: /docs/connectors/concepts/events-and-sync ## Event-Driven Architecture When work orders change in Field Nation, events trigger outbound synchronization to external systems. When external records change, webhooks trigger inbound synchronization to Field Nation. ```mermaid graph LR FN[Field Nation] -->|Event Stream| BROKER[Integration Broker] BROKER -->|Update| EXT[External System] EXT -->|Webhook| BROKER BROKER -->|Create/Update| FN ``` --- ## Available Events Field Nation publishes events for key work order lifecycle milestones. Configure which events trigger synchronization for each connector. ### Work Order Creation & Status --- ### Provider Assignment --- ### Work Execution --- ### Schedule & Timeline --- ### Updates & Changes --- ### Messages & Communication --- ### Financial Events --- ### Additional Events --- ## Configuring Event Triggers ### Selecting Events In the Integration Broker UI, configure which events trigger synchronization: ### Navigate to Event Configuration Go to your connector settings → Event Triggers section ### Select Events Check the events you want to trigger outbound sync: - ✅ `FN_WO_ASSIGNED` - Sync when provider assigned - ✅ `FN_WO_DONE` - Sync when work completed - ✅ `FN_WO_APPROVED` - Sync when work approved ### Configure Action For each event, specify the action: - **Update Record** - Update existing external record - **Create Record** - Create new external record (typically for messages/notes) - **Custom Script** - Run platform-specific logic ### Save Configuration Test event triggers with sample work orders --- ### Common Event Configurations **Minimal Sync (Status Only)** - `FN_WO_ASSIGNED` - Provider assigned - `FN_WO_DONE` - Work completed - `FN_WO_APPROVED` - Work approved **Standard Sync (Status + Communication)** - All minimal sync events - `FN_WO_MESSAGE_ADDED` - Sync messages - `FN_WO_SCHEDULE_UPDATED` - Sync schedule changes - `FN_WO_CUSTOM_FIELD_VALUE_UPDATED` - Sync custom fields **Comprehensive Sync (Full Lifecycle)** - All standard sync events - `FN_WO_ON_MY_WAY` - Provider en route - `FN_WO_PROVIDER_CHECKED_IN` - Check-in tracking - `FN_WO_PROVIDER_CHECKED_OUT` - Check-out tracking - `FN_WO_PAYMENT_APPROVED` - Financial tracking - `FN_WO_ATTACHMENT_ADDED` - Document sync --- ## Synchronization Patterns ### Inbound: External System → Field Nation External systems trigger synchronization via webhooks: ```mermaid sequenceDiagram participant Ext as External System participant WH as Webhook/Flow participant Broker as Integration Broker participant FN as Field Nation Ext->>WH: Record Created/Updated WH->>Broker: POST /integrations/trigger/{token} Broker->>Ext: GET Full Record Data Broker->>Broker: Apply Field Mappings Broker->>FN: Create/Update Work Order FN-->>Broker: Success Broker->>Ext: Update Sync Status (optional) ``` **Triggers:** - Record created in external system - Record status changed - Record field updated (specific fields) - Manual "Sync to Field Nation" button --- ### Outbound: Field Nation → External System Field Nation events trigger synchronization: ```mermaid sequenceDiagram participant FN as Field Nation participant EVT as Event Stream participant Broker as Integration Broker participant Ext as External System FN->>EVT: Work Order Event EVT->>Broker: Event Notification Broker->>Broker: Check Event Config Broker->>Broker: Apply Field Mappings Broker->>Ext: API Call (Update Record) Ext-->>Broker: Success Broker->>FN: Update Sync Status ``` **Triggers:** - Configured Field Nation events - Status changes - Custom field updates - Message additions --- ## Event Payloads Events include complete work order context: ```json { "event_type": "FN_WO_ASSIGNED", "event_timestamp": "2026-01-15T14:30:00Z", "workorder": { "id": 12345678, "title": "On-site Network Installation", "status": { "id": 2, "name": "assigned" }, "assignee": { "user": { "id": 987654, "first_name": "John", "last_name": "Technician", "email": "john@example.com", "phone": "+1-555-0100" } }, "schedule": { "service_window": { "start": { "utc": "2026-01-20 09:00:00" }, "end": { "utc": "2026-01-20 12:00:00" } } }, "pay": { "type": "fixed", "amount": 250.00 }, "custom_fields": [ /* ... */ ] }, "correlation_id": "ext-record-456" } ``` --- ## Best Practices ### Event Selection Begin with minimal sync (assigned, done, approved). Add more events after core sync is stable. **Phase 1**: Status changes only **Phase 2**: Add messages and schedule updates **Phase 3**: Add comprehensive tracking (check-in, expenses, etc.) Some events overlap. Choose the most specific event for your use case: - Use `FN_WO_STATUS_ROUTED` (once) instead of `FN_WO_ROUTED` (per provider) if you only need to know routing occurred - Use `FN_WO_ASSIGNED` if you don't need separate confirmation tracking High-frequency events can overwhelm external APIs: - `FN_WO_MESSAGE_ADDED` - Can fire frequently - `FN_WO_PROVIDER_CHECKED_IN/OUT` - Multiple per work order - `FN_WO_UPDATED` - May fire for many field changes Ensure your external system can handle the volume or implement batching. --- ### Synchronization Strategy **Bidirectional Consistency** - Map statuses bidirectionally (FN → External and External → FN) - Use correlation IDs to track related records - Handle sync conflicts (last-write-wins or manual resolution) **Idempotency** - Ensure same event processed multiple times produces same result - Check correlation ID before creating new records - Use upsert logic (create if not exists, update if exists) **Error Handling** - Configure retries for temporary failures - Set up dead letter queue monitoring - Alert on persistent sync failures --- ## Monitoring Event Sync ### Integration Logs Track event processing in Integration Broker logs: - Event received timestamp - Field mappings applied - API call made to external system - Success/failure status - Error details (if failed) ### Health Metrics Monitor key indicators: - **Event Processing Rate**: Events/minute - **Success Rate**: % successful syncs - **Average Latency**: Time from event to external update - **Error Rate**: % failed syncs - **Queue Depth**: Pending events in queue --- ## Troubleshooting Events **Check:** - Event enabled in connector configuration - Work order status actually changed to trigger event - Integration not paused/disabled - Event filters not excluding work order **Debug:** Review Integration Broker logs for event received confirmation **Causes:** - Some events naturally fire multiple times (ROUTED per provider, CHECK_IN per check-in) - Retry logic if external API failed - Webhook configured multiple times **Solution:** Implement idempotency in external system using correlation ID **Check:** - Field mappings configured for outbound sync - External API credentials valid - Correlation ID linking FN work order to external record - External API not rate limiting **Debug:** Check Integration Broker logs for API call details and response [Complete troubleshooting guide →](/docs/connectors/concepts/troubleshooting) --- --- ### Field mappings URL: /docs/connectors/concepts/field-mappings ## Mapping Fundamentals Every field mapping consists of: --- ## Transformation Actions ### Sync (ACTION_TYPE_SYNC) **Direct field-to-field copy with optional type conversion.** The simplest mapping action - copies the source field value directly to the target field. ```jsonnet { // Field Nation → External System "source": "workorder.title", "target": "external_system.job_name", "action": "sync" } ``` **Use Cases:** - Simple field copies (title → name, id → external_id) - Numeric values (amount → cost) - Boolean flags (is_active → active_status) --- ### Set Static (ACTION_TYPE_SET_STATIC) **Assign a predetermined static value regardless of source data.** Always sets the target field to a specific value. ```jsonnet { "target": "priority", "action": "set_static", "value": "High" } ``` **Use Cases:** - Default priority levels - Fixed category assignments - Constant flags (e.g., `source_system: "Field Nation"`) - Required fields without source data **Example: Always set status to "New"** ```jsonnet { "target": "status", "action": "set_static", "value": "New" } ``` --- ### Array Map (ACTION_TYPE_ARRAY_MAP) **Map source values to target values using exact match lookups.** Performs equality checks and returns corresponding mapped values. ```jsonnet { "source": "pay.type", "target": "payment_type", "action": "array_map", "mappings": [ { "compare": "fixed", "value": "Fixed Price" }, { "compare": "hourly", "value": "Hourly Rate" }, { "compare": "blended", "value": "Blended Rate" } ], "default": "Unknown" } ``` **Use Cases:** - Status code mapping (draft → New, assigned → Assigned) - Priority levels (1 → Low, 2 → Medium, 3 → High) - Category translation (service → Service Call, install → Installation) - Enum value conversion **Example: Work Order Status Mapping** ```jsonnet { "source": "status.name", "target": "external_status", "action": "array_map", "mappings": [ { "compare": "draft", "value": "New" }, { "compare": "assigned", "value": "Assigned" }, { "compare": "work_done", "value": "Completed" }, { "compare": "approved", "value": "Approved" } ], "default": "Unknown" } ``` --- ### Range Map (ACTION_TYPE_RANGE_MAP) **Map numeric values to categories based on range comparisons.** Uses less-than-or-equal comparisons from right to left. ```jsonnet { "source": "time_logs.hours", "target": "duration_category", "action": "range_map", "ranges": [ { "compare": 0.5, "value": "15-30 Minutes" }, { "compare": 1, "value": "31-60 Minutes" }, { "compare": 2, "value": "1-2 Hours" }, { "compare": 4, "value": "2-4 Hours" } ], "default": "4+ Hours" } ``` **How it works:** - Value `0.3` → "15-30 Minutes" (≤ 0.5) - Value `1.5` → "1-2 Hours" (≤ 2) - Value `5` → "4+ Hours" (default, exceeds all ranges) **Use Cases:** - Duration categorization - Cost tiers (0-100 → Low, 101-500 → Medium, 501+ → High) - Severity levels based on numeric scores - SLA priority based on response time --- ### Date Convert (ACTION_TYPE_DATE_CONVERT) **Transform date values between formats and timezones.** ```jsonnet { "source": "schedule.service_window.start.utc", "target": "scheduled_date", "action": "date_convert", "input_format": "YYYY-MM-DD HH:mm:ss", "output_format": "MM/DD/YYYY h:mm:ss A", "timezone_in": "UTC", "timezone_out": "America/New_York" } ``` **Common Date Formats:** - ISO 8601: `YYYY-MM-DDTHH:mm:ssZ` - US Format: `MM/DD/YYYY` - Full DateTime: `YYYY-MM-DD HH:mm:ss` - Unix Timestamp: `X` (seconds) or `x` (milliseconds) **Use Cases:** - UTC → Local timezone conversion - Format standardization (ISO → US format) - Date-only extraction from datetime fields - Adding/subtracting time offsets **Example: UTC to Local Business Hours** ```jsonnet { "source": "created_at", "target": "created_local", "action": "date_convert", "input_format": "YYYY-MM-DD HH:mm:ss", "output_format": "MM/DD/YYYY hh:mm A", "timezone_in": "UTC", "timezone_out": "America/Chicago" } ``` --- ### Concat (ACTION_TYPE_CONCAT) **Combine multiple fields into a single target field.** ```jsonnet { "target": "full_name", "action": "concat", "parts": [ { "type": "field", "value": "assignee.user.first_name" }, { "type": "static", "value": " " }, { "type": "field", "value": "assignee.user.last_name" } ] } ``` **Use Cases:** - Full names from first/last names - Address lines (street, city, state, zip) - Composite identifiers (prefix + ID + suffix) - Description building from multiple fields **Example: Full Address** ```jsonnet { "target": "full_address", "action": "concat", "parts": [ { "type": "field", "value": "location.address1" }, { "type": "static", "value": ", " }, { "type": "field", "value": "location.city" }, { "type": "static", "value": ", " }, { "type": "field", "value": "location.state" }, { "type": "static", "value": " " }, { "type": "field", "value": "location.zip" } ] } ``` --- ### Custom (ACTION_TYPE_CUSTOM) **Execute custom JSONNET code for complex transformations.** For logic that can't be expressed through standard actions, write custom JSONNET. ```jsonnet { "target": "priority_level", "action": "custom", "script": ||| local hours = $.util.lookup_field($.input, 'time_logs.hours', 0); local is_urgent = $.util.lookup_field($.input, 'priority', '') == 'urgent'; if is_urgent && hours > 2 then "Critical" else if is_urgent then "High" else if hours > 4 then "Medium" else "Low" ||| } ``` [Learn more about custom actions →](/docs/connectors/concepts/custom-actions) --- ## Mapping Patterns ### Bidirectional Mappings Configure separate mappings for each direction: **Inbound (External → Field Nation)** ```jsonnet { "source": "external_system.job_title", "target": "workorder.title", "action": "sync" } ``` **Outbound (Field Nation → External)** ```jsonnet { "source": "workorder.status.name", "target": "external_system.status", "action": "array_map", "mappings": [...] } ``` --- ### Nested Field Access Use dot notation to access nested objects: ```jsonnet { "source": "workorder.location.company.name", "target": "company_name", "action": "sync" } ``` Access array elements: ```jsonnet { "source": "workorder.contacts[0].email", "target": "primary_contact_email", "action": "sync" } ``` --- ### Custom Field Mappings Field Nation custom fields require special handling: **Mapping Custom Fields:** ```jsonnet { "action": "custom", "target": "external_field", "script": ||| $.util.lookup_custom_field('835', $.input, 'Default Value') ||| } ``` The `lookup_custom_field` function handles Field Nation's custom field structure automatically. [Explore Webhooks →](/docs/webhooks/introduction) --- ## Best Practices ### Field Mapping Strategy Begin with essential fields using Sync actions. Add complex transformations only after core synchronization works. **Phase 1**: Required fields (title, description, dates) **Phase 2**: Status and assignment mapping **Phase 3**: Custom fields and advanced logic Always provide default values for optional fields to prevent sync failures from missing data. ```jsonnet { "source": "optional_field", "target": "required_field", "action": "sync", "default": "N/A" } ``` Add comments to JSONNET scripts explaining business logic: ```jsonnet // Priority calculation: // - Urgent + >2 hours = Critical // - Urgent only = High // - >4 hours = Medium // - Otherwise = Low ``` When adding new mappings: 1. Test the mapping in isolation 2. Verify source data availability 3. Confirm target field accepts the value 4. Check edge cases (null, empty, unexpected values) ### Performance Considerations **Keep Mappings Efficient:** - ✅ Use native actions (Sync, Array Map) when possible - ✅ Minimize custom JSONNET complexity - ✅ Cache repeated calculations in local variables - ⚠️ Avoid nested loops in custom actions - ⚠️ Limit external API calls within mappings --- ## Common Mapping Scenarios ### Status Synchronization Map bidirectional status values: ```jsonnet // Inbound: External → Field Nation { "source": "status", "target": "status_id", "action": "array_map", "mappings": [ { "compare": "New", "value": "1" }, { "compare": "Assigned", "value": "2" }, { "compare": "Completed", "value": "5" } ] } // Outbound: Field Nation → External { "source": "status.id", "target": "external_status", "action": "array_map", "mappings": [ { "compare": "1", "value": "Open" }, { "compare": "2", "value": "In Progress" }, { "compare": "5", "value": "Closed" } ] } ``` ### Contact Information Combine or split contact fields: ```jsonnet // Combine first + last name { "target": "full_name", "action": "concat", "parts": [ { "type": "field", "value": "user.first_name" }, { "type": "static", "value": " " }, { "type": "field", "value": "user.last_name" } ] } // Extract email domain { "action": "custom", "target": "email_domain", "script": ||| local email = $.util.lookup_field($.input, 'user.email', ''); local parts = std.split(email, '@'); if std.length(parts) > 1 then parts[1] else '' ||| } ``` ### Financial Data Handle currency and decimal precision: ```jsonnet { "source": "pay.amount", "target": "cost", "action": "custom", "script": ||| local amount = $.util.lookup_field($.input, 'pay.amount', 0); std.round(amount * 100) / 100 // Round to 2 decimal places ||| } ``` --- ## Field Discovery The Integration Broker automatically discovers available fields from external systems. ### Automatic Field Discovery 1. Navigate to Integration Settings 2. Select your connector 3. Click "Refresh Fields" 4. System queries external API metadata 5. Available fields populate in dropdown menus ### Field Types Discovered - **Standard Fields**: Built-in platform fields - **Custom Fields**: User-defined fields - **Related Objects**: Parent/child relationships - **Picklist Values**: Available enum values - **Required Fields**: Marked for validation --- ## Troubleshooting **Possible Causes:** - Source field path incorrect (check dot notation) - Source field empty/null (add default value) - Target field not writable (check permissions) - Data type mismatch (add conversion logic) **Debug:** ```jsonnet // Log the source value { "action": "custom", "target": "debug_output", "script": ||| local value = $.util.lookup_field($.input, 'source.path', 'NOT_FOUND'); std.trace("Source value: " + std.toString(value), value) ||| } ``` **Possible Causes:** - Case sensitivity (compare: "new" vs "New") - Extra whitespace in source value - Numeric vs string comparison - Missing default value **Solution:** ```jsonnet { "action": "custom", "target": "status", "script": ||| local raw = $.util.lookup_field($.input, 'status', ''); local normalized = std.asciiLower(std.stripChars(raw, ' ')); $.util.array_map([...], normalized, 'Unknown') ||| } ``` **Possible Causes:** - Input format doesn't match actual data format - Timezone string invalid - Null/empty date value **Solution:** Add validation before conversion: ```jsonnet { "action": "custom", "target": "converted_date", "script": ||| local date_str = $.util.lookup_field($.input, 'date_field', ''); if date_str != '' then $.util.date_convert(date_str, 'YYYY-MM-DD', 'MM/DD/YYYY') else '' ||| } ``` --- --- ### Troubleshooting URL: /docs/connectors/concepts/troubleshooting ## Quick Diagnosis **Work orders not creating or updating?** ### Check Webhook Verify webhook triggering in external system: - Check webhook configuration - Review webhook logs/history - Test webhook manually ### Verify Authentication Confirm credentials are valid: - API keys not expired - OAuth tokens refreshed - Correct instance/account ID - Sufficient permissions ### Review Logs Check Integration Broker logs: - Navigate to Integrations → Logs - Filter by date/time of expected sync - Look for authentication or validation errors ### Test Manually Trigger a test sync: - Create a simple test record - Verify it meets trigger conditions - Monitor logs for processing **Some fields syncing, others not?** ### Check Field Mappings Review configured mappings: - Is the field mapped? - Is the mapping action correct? - Does source field exist in payload? ### Verify Permissions Ensure API user can access fields: - Read permission on source fields - Write permission on target fields - Custom field API access enabled ### Test Field Values Inspect actual data: - Check source field contains value - Verify data type matches expectation - Test transformation logic with sample data ### Review Logs Look for field-specific errors: - Validation failures - Type conversion errors - JSONNET execution errors **Seeing errors in logs?** ### Identify Error Category Determine error type: - Authentication (401, 403) - Validation (400, 422) - Rate Limiting (429) - Server Error (500, 502, 503) ### Check Error Details Review error message and context: - Which API endpoint failed? - What data was being sent? - Is error consistent or intermittent? ### Apply Solution Follow category-specific fix: - Authentication → Refresh credentials - Validation → Fix field mappings - Rate Limit → Add delays/batching - Server Error → Retry, contact support ### Monitor Resolution Verify fix resolves issue: - Trigger test sync - Watch for successful processing - Confirm no new errors **Syncs are slow or delayed?** ### Check Queue Depth Review message queue status: - Navigate to Integrations → Metrics - Check pending message count - Identify bottlenecks ### Monitor API Response Times Review external API performance: - Check Integration Broker logs for latency - Test external API directly - Verify no rate limiting ### Optimize Mappings Reduce transformation complexity: - Simplify custom JSONNET - Remove unnecessary field mappings - Batch operations where possible ### Scale Resources If consistently slow: - Contact Field Nation Support - Request additional worker instances - Review architecture with support team --- ## Common Issues & Solutions ### Authentication Problems **Symptoms**: All sync attempts fail with 401 error **Causes:** - API credentials invalid or expired - Wrong username/password - OAuth token expired - API key revoked **Solutions:** 1. Regenerate API credentials in external system 2. Update credentials in Integration Broker 3. For OAuth: Reauthorize integration 4. Verify account is active in external platform **Test:** ```bash # Test API credentials directly curl -H "Authorization: Bearer YOUR_TOKEN" \ https://external-api.com/api/test ``` **Symptoms**: Authentication succeeds but API calls fail **Causes:** - Insufficient API permissions - User not assigned required role - IP whitelisting blocking requests - API feature not enabled for account **Solutions:** 1. Review API user permissions in external system 2. Grant required roles/permissions 3. Whitelist Field Nation IP addresses 4. Enable API access features **Field Nation IPs to Whitelist:** - Sandbox: Check [Environments](/docs/resources/environments) - Production: Check [Environments](/docs/resources/environments) **Symptoms**: Integration works initially, then stops **Causes:** - Refresh token expired - OAuth app configuration changed - User revoked access **Solutions:** 1. Reauthorize integration in Integration Broker 2. Verify OAuth app still exists in external system 3. Check OAuth app client ID/secret unchanged 4. Ensure refresh token URL correct --- ### Data Validation Errors **Symptoms**: 400 error - "Required field missing" **Causes:** - Required field not mapped - Source field empty/null - Field mapping action incorrect **Solutions:** 1. Identify missing field from error message 2. Add field mapping for required field 3. Set default value if source may be empty 4. Use Set Static action for constant values **Example:** ```jsonnet { "target": "required_field", "action": "set_static", "value": "Default Value" } ``` **Symptoms**: 422 error - "Invalid value for field" **Causes:** - String sent to numeric field - Invalid date format - Boolean as string ("true" vs true) - Value outside allowed range **Solutions:** 1. Check target field expected type 2. Add type conversion in custom action: ```jsonnet { "action": "custom", "target": "numeric_field", "script": ||| local value = $.util.lookup_field($.input, 'source', '0'); std.parseInt(value) ||| } ``` 3. Use Date Convert action for dates 4. Use Range Map for numeric categories **Symptoms**: Field not accepting value - "Invalid option" **Causes:** - Mapped value not in picklist - Case sensitivity mismatch - Picklist values changed in external system **Solutions:** 1. Refresh field metadata in Integration Broker 2. Review valid picklist values 3. Update array map definitions 4. Use exact case matching **Example:** ```jsonnet { "action": "array_map", "mappings": [ { "compare": "high", "value": "High" }, // External expects "High" { "compare": "low", "value": "Low" } ], "default": "Medium" } ``` --- ### Webhook & Trigger Issues **Symptoms**: No events reaching Integration Broker **Causes:** - Webhook not configured in external system - Trigger conditions not met - Webhook URL incorrect - Firewall blocking outbound requests **Solutions:** 1. Verify webhook exists in external system 2. Check webhook URL matches Integration Broker trigger URL 3. Test webhook with manual trigger 4. Review external system webhook logs 5. Check network/firewall rules **Symptoms**: External system reports webhook timeout **Causes:** - Integration Broker slow to respond - High queue depth - External API calls taking too long **Solutions:** 1. Integration Broker responds quickly ( **Symptoms**: Same external record creates multiple work orders **Causes:** - Webhook triggering multiple times - No correlation ID check - Retry logic creating duplicates **Solutions:** 1. Implement idempotency using correlation ID 2. Check Integration Broker logs for duplicate triggers 3. Review webhook configuration (should trigger once per record) 4. Enable duplicate detection in Integration Broker --- ### Field Mapping Problems **Symptoms**: Transformation fails, JSONNET syntax error **Common Errors:** - Syntax error: Missing comma, bracket mismatch - Runtime error: Null pointer, type mismatch - Logic error: Wrong output, unexpected value **Debug Steps:** 1. Test JSONNET in isolation with sample payload 2. Add `std.trace()` statements to inspect values 3. Use local variables to break down complex logic 4. Check Integration Broker logs for error details **Debug Example:** ```jsonnet { result: local value = $.util.lookup_field($.input, 'field', 'default'); local traced = std.trace("Value: " + std.toString(value), value); // Continue with traced value traced } ``` **Symptoms**: lookup_field returns default value **Causes:** - Incorrect path syntax - Field doesn't exist in payload - Array access incorrect **Solutions:** 1. Check payload structure in logs 2. Verify dot notation path correct 3. Use array index for array fields: ```jsonnet // Correct $.util.lookup_field($.input, 'contacts[0].email', '') // Incorrect $.util.lookup_field($.input, 'contacts.email', '') ``` **Symptoms**: Date field empty or error **Causes:** - Input format doesn't match actual format - Timezone string invalid - Source date null/empty **Solutions:** 1. Validate date format in source data 2. Add null check before conversion: ```jsonnet { "action": "custom", "target": "converted_date", "script": ||| local date = $.util.lookup_field($.input, 'date_field', ''); if date != '' then $.util.date_convert(date, 'YYYY-MM-DD', 'MM/DD/YYYY') else '' ||| } ``` 3. Use try-catch pattern for robust handling --- ### Performance Issues **Symptoms**: Messages backing up in queue **Causes:** - External API slow - Complex field mappings - High volume of events - Worker instances insufficient **Solutions:** 1. Review external API performance 2. Simplify field mappings where possible 3. Batch operations if supported 4. Contact Field Nation Support for scaling **Symptoms**: 429 errors from external API **Causes:** - Too many API calls per minute - Burst limit exceeded - Per-user limits hit **Solutions:** 1. Configure rate limits in Integration Broker 2. Reduce sync frequency 3. Batch updates where possible 4. Request rate limit increase from external platform **Symptoms**: Long delay between trigger and sync **Causes:** - Queue backed up - External API latency - Complex transformations - Network issues **Debug:** 1. Check Integration Broker metrics for bottleneck 2. Review logs for timing breakdown 3. Test external API directly for response time 4. Simplify field mappings as test --- ## Debugging Checklist Use this systematic approach for any integration issue: ### 1. Gather Information - ☐ When did the issue start? - ☐ Is it affecting all syncs or specific records? - ☐ What changed recently? (configuration, external system, Field Nation) - ☐ Can you reproduce the issue? ### 2. Check Logs - ☐ Integration Broker logs for error messages - ☐ External system webhook logs - ☐ Field Nation work order history - ☐ Network/firewall logs if connectivity suspected ### 3. Test Components - ☐ Test authentication (API credentials work?) - ☐ Test webhook (manual trigger works?) - ☐ Test field mappings (with sample data) - ☐ Test external API directly (outside integration) ### 4. Isolate Problem - ☐ Is it authentication, validation, or transformation? - ☐ Is it specific field or all fields? - ☐ Is it inbound, outbound, or both? - ☐ Is it consistent or intermittent? ### 5. Apply Fix - ☐ Update configuration based on findings - ☐ Test fix with sample sync - ☐ Monitor for successful processing - ☐ Document solution for future reference --- ## Getting Help ### Before Contacting Support Gather this information to expedite support: 1. **Integration Details** - Connector name (Salesforce, ServiceNow, etc.) - Company ID - Environment (Sandbox or Production) 2. **Issue Description** - What you expected to happen - What actually happened - When the issue started - Frequency (always, sometimes, once) 3. **Relevant IDs** - Work Order ID(s) affected - External Record ID(s) - Correlation ID(s) - Error message timestamps 4. **Troubleshooting Attempted** - Steps you've already tried - Results of each attempt - Any temporary workarounds ### Contact Support **Field Nation Support:** - **Portal**: [app.fieldnation.com/support-cases](https://app.fieldnation.com/support-cases) - **Phone**: +1 877-573-4353 (24/7) **Priority Levels:** - **P1 (Critical)**: Integration completely down, business impact - **P2 (High)**: Partial failure, workaround available - **P3 (Medium)**: Minor issue, low impact - **P4 (Low)**: Question, enhancement request [Complete support resources →](/docs/resources/support) --- ## Platform-Specific Troubleshooting Each platform has unique considerations: Flow and Outbound Message issues Business Rule and REST Message debugging OpenAPI spec and endpoint configuration [View all platforms →](/docs/connectors/introduction) --- ## Preventive Measures ### Monitoring - ✅ Set up error email notifications - ✅ Monitor queue depths daily - ✅ Review sync success rates weekly - ✅ Check external API health ### Maintenance - ✅ Test integrations after external system upgrades - ✅ Rotate API credentials periodically - ✅ Review and update field mappings - ✅ Document configuration changes ### Best Practices - ✅ Start simple, add complexity gradually - ✅ Test thoroughly in sandbox before production - ✅ Keep transformation logic simple - ✅ Use descriptive names for mappings - ✅ Document business logic in JSONNET comments --- --- ### Configuration URL: /docs/connectors/platforms/autotask/configuration Complete the steps below in order. **Steps 1–5 are completed inside Autotask** by an Autotask administrator and can be handed off to them independently. Once they hand you the credentials, complete Steps 6–8 inside Field Nation. **Estimated setup time:** 30–60 minutes for an Autotask administrator familiar with security roles. > [INFO] Before starting, confirm your Autotask edition includes API access and that you know whether you need a custom Integration Code. See [Before you begin](/docs/connectors/platforms/autotask/overview#before-you-begin) on the Overview page. --- ## Step 1: Verify Autotask edition and API access Your Autotask subscription must include API access. This is available on **Pro and Enterprise** editions. Essentials-tier subscriptions do not include API access. Confirm API access is available using any of these methods: - **Admin → Resources (Users) → New Resource** — if **API User** appears as a Resource Type option, API access is included. - **Admin → Security Levels → New Security Level** — if **API User** is selectable as the license type, API access is available. - **Admin → Features & Settings** — confirm your edition is listed as Pro or Enterprise. If none of these show API User, your subscription lacks API access. Contact your Autotask administrator to add it. --- ## Step 2: Create a dedicated API security level Create a **new** Security Level specifically for the Field Nation integration. Do **not** reuse an existing employee, technician, or admin security level. **In Autotask:** Admin → Account Settings & Users → Resource/Users (HR) → Security Levels | Setting | Value | |---|---| | Security Level Name | `Field Nation Integration` (or similar descriptive name) | | License Type | **API User** | ### Service Desk module permissions | Permission | Required level | |---|---| | Tickets — View | All | | Tickets — Add | Yes | | Tickets — Edit | Yes | | Tickets — Delete | No | | Ticket Notes — View | All | | Ticket Notes — Add | Yes | | Ticket Notes — Edit | No | | Ticket Costs (Charges) — View | All | | Ticket Costs (Charges) — Add | Yes | | Ticket Costs (Charges) — Edit | Yes | | Attachments — View / Download | Yes | | Attachments — Add | Yes | | Attachments — Delete | Yes | > [INFO] **Note on Time Entry permissions:** Time Entries are **not** managed in the Service Desk module. Set these under the Contracts or Timesheets module — see the section below. **Note on Ticket Costs and Attachments:** These may not appear as separate items in all Autotask editions. If not visible in Service Desk, check under Tickets sub-permissions or their respective modules. ### CRM module permissions (read-only) These are required for account and contact lookups during ticket import. | Permission | Required level | |---|---| | Accounts (Companies) — View | All | | Account Physical Locations — View | All | | Contacts — View | All | ### Contracts / Timesheets / Expense module permissions | Permission | Required level | |---|---| | Time Entries — View | All | | Time Entries — Add | Yes | | Time Entries — Edit | Yes | | Time Entries — Delete | Yes | | Expenses — View | All (required only if expense sync is enabled) | | Expenses — Add | Yes (required only if expense sync is enabled) | | Expenses — Edit | All, or Mine (required only if expense sync is enabled) | ### Other permissions (read-only reference data) | Permission | Required level | |---|---| | Resources (Users) — View | Yes | > **Do not grant beyond what is listed above.** The following permissions are not required and should not be granted: Admin / System Settings access, user management, contract management (create/edit/delete), project management, billing/invoicing, or queue management. Roles and Service Calls permissions may not appear in all Autotask editions — if not visible, skip them. --- ## Step 3: Create the API user **In Autotask:** Admin → Resources (Users) → API User | Field | Value | |---|---| | Resource Type | **API User** | | Security Level | The `Field Nation Integration` security level created in Step 2 | | Username (Key) | Service account format, e.g. `svc-fieldnation@yourdomain.com` | | Password (Secret) | Minimum 16 characters, alphanumeric + standard symbols. **Avoid ``, and `&`** — these can cause authentication errors. | | Status | **Active** | | Department | Required by Autotask — use IT or Integrations | | Queue / Team assignment | Do **not** assign — this is an API-only user | ### API Tracking Identifier The **API Tracking Identifier** section appears on the Add API User form. This field is required for API v1.6 and later. **Once set, it cannot be changed.** | Field | Value | |---|---| | Tracking Identifier type | **Integration Vendor** | | Integration Vendor | **Field Nation - Pre-packed integration with Autotask** | > Select **Integration Vendor** — not "Custom (Internal Integration)". Then choose **"Field Nation - Pre-packed integration with Autotask"** from the dropdown. This links the API user to Field Nation's registered partner integration code automatically. If this field is not set correctly, the connector will fail to authenticate. ### Line of Business The Line of Business section controls which Autotask data this API user can access. Leave all items under **Not Associated** unless your organisation uses Line of Business to restrict data visibility. Check **"Resource can view items with no assigned Line of Business"** to ensure the API user can access all relevant tickets. > [INFO] **Security recommendation:** Use a dedicated email alias for this service account (for example, `autotask-integrations@yourdomain.com`) so that password reset notifications do not go to a personal inbox. If the account is deactivated or the password changes, the integration will stop working. --- ## Step 4: Determine your integration code Field Nation provides a registered partner integration code by default. **For most customers, no action is required** — leave the Integration Code field blank in the Field Nation connector and the Field Nation partner code is used automatically. You only need to act if your organization requires a **customer-specific integration code**: 1. Obtain your code from Autotask Support or your account manager. 2. Enter it in the **Integration Code** field in Step 6. If you are unsure, leave the field blank. You can update it later without disrupting the connection. --- ## Step 5: Verify network access The Field Nation integration service makes outbound HTTPS (port 443) calls to your Autotask zone URL (for example, `webservices2.autotask.net` or `webservices4.autotask.net`). - **Cloud-hosted Autotask (standard SaaS):** No firewall changes are typically required. - **Network policy restricts outbound API access by source IP:** Contact your Field Nation account team for the Field Nation service IP range, then allowlist your Autotask zone base URL on port 443. --- ## Step 6: Configure the Field Nation connector Once Steps 1–5 are complete, log in to Field Nation as a Company Admin and navigate to **Integrations → Field Services → Autotask**. Enter the values below. Use the input fields to save them to your browser — they'll appear in the [reference sheet](#your-reference-sheet) at the bottom of this page. **Password** — Enter the API user password directly in the Field Nation connector settings. Do not store it here. Use the password set in Step 3 — minimum 16 characters, no ``, or `&` characters. > **Time entry sync is only supported when Service Object Name is set to `ticket`.** If you select `ServiceCall` or `Project`, ticket create/update and field mappings will work, but all time entry writes will be silently skipped — no error is raised and no alert is sent. Only use `ServiceCall` or `Project` if time entry sync is not required for your workflow. Save the configuration. The connector performs zone discovery automatically — it resolves your Autotask instance URL from the username and does not require you to enter it manually. --- ## Step 7: Get your trigger URL The trigger URL is the endpoint Autotask POSTs ticket IDs to when you want automated inbound imports — for example, when a ticket reaches a specific status. **Field Nation provides this URL.** Your Field Nation account team will supply your trigger URL directly. You can also generate one yourself from within the connector settings: **In Field Nation:** Integrations → Field Services → Autotask → Trigger URL The URL follows this pattern: ``` https://micro.fieldnation.com/v1/broker/inbound?client_token={your-client-token} ``` Copy and store the full URL including the security token. The token authenticates all inbound requests — treat it as a secret. > **The trigger URL is required for automated inbound operations (Autotask → Field Nation).** Ensure you have successfully retrieved the trigger URL from your Field Nation integration settings before configuring the Autotask webhook. Without it, the Autotask → Field Nation flow will not function automatically. > [INFO] Without a trigger URL configured in Autotask, inbound imports are initiated manually — a Field Nation admin provides the ticket ID on demand. If your team wants Autotask to push tickets automatically (for example, when a ticket reaches a specific status), configure an Autotask webhook to POST the ticket ID to this URL. --- ## Step 8: Refresh fields Before configuring field mappings, click **Refresh Fields** to discover the available fields from your Autotask instance. **In Field Nation:** Integrations → Field Service → Autotask → Manage → Settings → Refresh Fields The connector queries the Autotask API and retrieves: - Standard fields — Title, Description, Status, Priority, Due Date, and others on the Ticket entity - User-Defined Fields (UDFs) configured on your selected entity type - Related entity fields — Account, Contact, Resource (assigned technician) - Picklist values that populate the mapping dropdowns > If you add new UDFs to Autotask after the connector is set up, click **Refresh Fields** again to make them available. The connector discovers UDFs at connection time only. --- ## Step 9: Configure field mappings Field mappings define how data translates between Autotask and Field Nation. Configure mappings for both directions in **Integrations → Field Services → Autotask → Field Mappings**. ### Inbound (Autotask → Field Nation) Runs when an Autotask ticket is imported into Field Nation. | Autotask field | Field Nation field | Notes | |---|---|---| | `Title` | `title` | Direct copy | | `Description` | `description` | Direct copy | | `Status` | `status_id` | Use Status Mapping (Step 10) | | `DueDateTime` | `time_window.end` | Date Convert action recommended | | `Account.AccountName` | `location.name` | Related entity field | | `UserDefinedField.{udf_name}` | Custom field | UDFs discovered after Refresh Fields | ### Outbound (Field Nation → Autotask) Runs when a Field Nation work order event fires (time entry logged, expense submitted, etc.). | Field Nation field | Autotask field | Notes | |---|---|---| | `title` | [`Ticket:Title`](#required-fields-for-outbound-create) | Required for Outbound Create | | `status` | [`Ticket:Status`](#required-fields-for-outbound-create) | Required for Outbound Create — values are Autotask status IDs | | — | [`Ticket:Priority`](#required-fields-for-outbound-create) | Required for Outbound Create — sourced from Autotask | | — | [`Ticket:AccountID`](#required-fields-for-outbound-create) | Required for Outbound Create — sourced from Autotask | | — | [`Ticket:QueueID`](#required-fields-for-outbound-create) | Required for Outbound Create — sourced from Autotask | | `time_window.start` | [`Ticket:DueDateTime`](#required-fields-for-outbound-create) | Required for Outbound Create | | `notes` | `Resolution` | Direct copy | | `time_logs[0].StartDateTime` | `TimeEntry.[0].StartDateTime` | Array field — index replaces `[0]` at runtime | | `expenses[0].amount` | `expense.items[0].ExpenseAmount` | Array field | ### Required fields for Outbound Create When **Outbound Create** is enabled, every new Field Nation work order creates an Autotask ticket via the SOAP API. Autotask requires six fields on every ticket creation call. If any are missing, the sync fails with an error and no ticket is created. > These six mappings are **mandatory** when Outbound Create is enabled. Missing any one will silently fail ticket creation — you will receive a notification email if one is configured, but no retry occurs. > [INFO] **Testing tip:** You can use **Set Static Values** with a hardcoded ID to get started quickly and verify the connection works end-to-end. Once confirmed, replace the static value with a **custom field** mapping if your workflow requires dynamic values per work order. | Autotask required field | How to get the value | |---|---| | `Ticket:Title` | Map from the Field Nation work order title (`title`). Use **Sync Values** for a direct copy, or **Concat Values** to combine fields. | | `Ticket:Status` | **Sourced from your Autotask instance.** Represents the ticket status on creation. The valid values are the numeric status IDs from your Autotask instance — see the [Default Autotask status list](#configure-the-mapping) in Step 10 for reference. Use **Set Static Values** to assign a fixed status (e.g. `New`) when a ticket is created, or **Array Map** to translate Field Nation status values to Autotask status IDs. | | `Ticket:Priority` | **Sourced from your Autotask instance.** Priority IDs are discovered via **Refresh Fields** after connecting. Use **Set Static Values** with a hardcoded Autotask Priority ID to test, or a custom field for dynamic mapping. | | `Ticket:AccountID` | **Sourced from your Autotask instance.** Navigate to **Admin → Accounts → [Account name]** in Autotask. Copy the Account ID from the URL or account details panel. Use **Set Static Values** with the Autotask Account ID to test, or a custom field to route to different accounts per work order. | | `Ticket:QueueID` | **Sourced from your Autotask instance.** Navigate to **Admin → Service Desk → Queues → [Queue name]** in Autotask. Copy the Queue ID from the URL. Use **Set Static Values** with the Autotask Queue ID to test, or a custom field for dynamic routing. Alternatively, supply both `AssignedResourceID` and `AssignedResourceRoleID` in place of a queue ID. | | `Ticket:DueDateTime` | Map from the work order's scheduled start date in Field Nation (`time_window.start`). Use **Sync Values** for a direct copy, or **Date Convert** if a timezone conversion is required. | **How to add each required mapping:** 1. In the field mapping UI, select the target Autotask field (e.g. `Ticket:AccountID`) from the right dropdown. 2. Choose the action type that fits your requirement — refer to the table above for guidance per field. 3. Enter the value or configure the mapping as required. 4. Save — the value will be sent on every outbound ticket creation. > [INFO] `Ticket:AccountID` and `Ticket:QueueID` are specific to your Autotask instance. Every customer will have different values. Do not copy these IDs from another organisation's configuration. ### Adding a mapping ### Select source field Choose the **Field Nation field** from the left dropdown (inbound) or the Autotask field from the left dropdown (outbound). ### Select target field Choose the corresponding field in the other system from the right dropdown. ### Choose action type | Action type | When to use | |---|---| | **Direct** | Values are identical or require no transformation | | **Array Map** | Value needs to be translated (e.g., picklist label → enum value) | | **Status Mapping** | Autotask status values → Field Nation status IDs | | **Date Convert** | Date format or timezone conversion required | | **Concat Values** | Combine multiple fields into one | | **Custom Action** | Complex transformation via Jsonnet | ### Save Click **Save Mapping**. Repeat for all required fields. --- ## Step 10: Configure status mapping Status mapping translates Autotask status picklist values to Field Nation status IDs. Configure it under **Field Mappings → Status** in the connector settings. ### Find your Autotask status IDs Autotask stores each status as a numeric ID internally. The status label shown in the UI (for example, "In Progress") is not the same as the ID the API uses. You need the numeric ID when configuring the mapping. **Method A — Browser inspector (fastest)** ### Open the Autotask status admin page Log in to Autotask as an administrator and navigate to **Admin → Features & Settings → Task & Ticket Statuses**. ### Open browser developer tools Press **F12** (Windows) or **Cmd + Option + I** (Mac) to open the developer tools panel, then select the **Elements** tab. ### Inspect a status row Click the inspector cursor icon and click on any status row in the table. In the Elements panel, locate the `` element — it will have a `data-row-key` attribute. That numeric value is the status ID. ```html ``` In this example, the status with `data-row-key="10"` has internal ID `10`. Repeat for each status you want to map. **Method B — Autotask REST API** Query the Ticket entity's field definitions to retrieve all status picklist values and their IDs in one call: ```sh curl -s \ -H "UserName: YOUR_API_USERNAME" \ -H "Secret: YOUR_API_PASSWORD" \ -H "ApiIntegrationCode: YOUR_INTEGRATION_CODE" \ "https://YOUR_ZONE.autotask.net/atservicesrest/v1.0/Tickets/entityInformation/fields" \ | python3 -c " import json, sys fields = json.load(sys.stdin)['fields'] status = next(f for f in fields if f['name'] == 'Status') for v in status['picklistValues']: print(v['value'], '-', v['label']) " ``` Replace `YOUR_ZONE` with your zone URL (for example, `webservices2`). The output lists every status ID and label in your instance: ``` 1 - New 5 - In Progress 10 - Waiting Customer 20 - Complete 21 - Cancelled ``` > [INFO] Your zone URL is in the response from the zone discovery command on the Overview page. If you ran the curl command during setup, the `url` field contains your base URL. ### Configure the mapping Once you have your status IDs, click **Refresh Fields** (**Integrations → Field Service → Autotask → Manage → Settings → Refresh Fields**) to load the live status values, then configure each mapping. > **Status IDs are unique per Autotask instance.** The same status name ("Complete", "In Progress") will have a different numeric ID in every Autotask account. The connector codebase confirms this — no standard ID mapping exists. Always look up your actual IDs using Method A or B above. Never copy IDs from another organisation's configuration. **Two connector defaults to know before mapping:** | Behaviour | Value | Source | |---|---|---| | Outbound Create — new ticket default status | `1` (New) | Hardcoded in connector | | Tickets excluded from inbound load | Status marked as Complete | Resolved by your instance's Complete ID | The table below lists the default statuses from a typical Autotask configuration. Use it as a reference when configuring your mappings — your instance may have additional or differently named statuses. | # | Autotask status | SLA event | |---|---|---| | 1 | Assigned to Contract | First Response | | 2 | New | — | | 3 | Acknowledged | First Response | | 4 | Client No-Show | Waiting Customer | | 5 | Dispatched | First Response | | 6 | Help Desk | First Response | | 7 | Missed Appointment | First Response | | 8 | Waiting Dispatch | First Response | | 9 | 10% Done | First Response | | 10 | 25% Done | First Response | | 11 | 50% Done | First Response | | 12 | In Progress | Resolution Plan | | 13 | 90% Done | First Response | | 14 | Escalate to Vendor | Waiting Customer | | 15 | 95% Done | First Response | | 16 | Followup | Resolution Plan | | 17 | Waiting Parts | Waiting Customer | | 18 | Verification | Resolution Plan | | 19 | Waiting Client | Waiting Customer | | 20 | Complete | Resolved | | 21 | Complete Send Survey | Resolved | | 22 | Cancelled | — | --- ## Step 11: Configure array map Array Map transforms a source field value to a target value using a JSON lookup table. Use it wherever you need to translate between Autotask picklist labels and Field Nation enum values — for example, priority levels, issue types, or custom UDF values. In the connector's Field Mappings section, select **Array Map** as the action type and enter a JSON configuration: ```json { "source_field": "Priority", "target_field": "priority", "map": [ { "source_value": "Critical", "target_value": "urgent" }, { "source_value": "High", "target_value": "urgent" }, { "source_value": "Medium", "target_value": "normal" }, { "source_value": "Low", "target_value": "low" } ], "default": "normal" } ``` | Property | Required | Description | |---|---|---| | `source_field` | Yes | Field name in the source system | | `target_field` | Yes | Field name in the target system | | `map` | Yes | Array of `source_value` → `target_value` pairs | | `default` | No | Value used when no match is found; omit to leave the field empty on no match | **Common uses for Array Map:** - Autotask priority labels → Field Nation priority values - Autotask issue type → Field Nation work order type - Custom UDF picklist values → Field Nation custom fields - Any value translation that cannot be handled with a Direct mapping --- ## Step 12: Optional feature configuration ### Outbound Create By default, the connector only syncs data to Autotask tickets that were previously imported into Field Nation (inbound flow). Enable **Outbound Create** to have every new Field Nation work order automatically create a new Autotask ticket. **Field Nation → Integrations → Field Services → Autotask → Manage → Settings → Outbound Create** (toggle) Requires the API user to have **Tickets — Add** permission (Step 2). ### Expenses/charges routing When a provider submits an expense or charge in Field Nation, configure where it lands in Autotask. Only one destination applies per connector — you cannot split routing between both. For full details on what data syncs for each destination, see [Expenses/charges sync](/docs/connectors/platforms/autotask/workflow#expensescharges-sync). | Setting | Destination in Autotask | Entity written | |---|---|---| | Blank (default) | Expense module | ExpenseReport + ExpenseItem | | `charge` | Service Desk | TicketCost | **Field Nation → Integrations → Field Services → Autotask → Manage → [Additional Charges](/docs/connectors/platforms/autotask/guides/charge-sync)** Requires the API user to have the corresponding permissions (Expense module or Ticket Costs) from Step 2. ### Attachment sync If attachment sync is enabled for your account, files uploaded to a Field Nation work order are synced to the Autotask ticket automatically. Contact your Field Nation account team to enable attachment sync. - Files **≤ 5 MB** are uploaded as embedded file attachments (`FILE_ATTACHMENT`). - Files **> 5 MB** are synced as URL links (`URL`). - Executable and archive file types are always synced as URL links — this is an Autotask API restriction. - Signature captures are always synced as embedded file attachments with public visibility. Requires the API user to have **Attachments — View, Add, and Delete** permissions (Step 2). --- ## Step 13: Validate the connection There is no separate Test Connection button. When you **save** the connector configuration, Field Nation automatically tests the connection using the credentials you entered. After saving: 1. Check the connector status — if authentication succeeds, the connector shows as active with no errors. 2. If credentials are invalid, an error is shown immediately on save. 3. Trigger a test sync — for example, log a time entry on a linked work order. 4. Verify the test record appears in Autotask as expected (check the ticket for a new TimeEntry). 5. Once validated, click the **Enable Sync** button to activate the connector. Sync will not run until this is enabled. **If the connection fails:** | Symptom | Likely cause | Resolution | |---|---|---| | Authentication fails entirely | Resource Type is not set to API User | Change the resource type to **API User** in Autotask Admin | | Authentication errors on specific operations | Password contains ``, or `&` | Reset the password using only alphanumeric characters and standard symbols | | Permission denied or authentication rejected | Integration code mismatch | Try leaving the Integration Code blank to use the Field Nation default | | Work orders not creating in Autotask | Ticket Add permission missing or Outbound Create is off | Grant Ticket Add permission and enable the Outbound Create toggle | | Time entry deletions not reflected in Autotask | Time Entry Delete permission missing | Add Time Entry Delete permission to the security level | | Expense sync errors | Expense permissions not granted | Add Expense permissions to the security level | | Connection times out | Network egress restriction | Allow outbound port 443 to `*.autotask.net` | --- ## Minimum permissions reference | Entity | View | Add | Edit | Delete | |---|---|---|---|---| | Ticket | ✓ | ✓ | ✓ | | | TimeEntry | ✓ | ✓ | ✓ | ✓ | | TicketNote | ✓ | ✓ | | | | TicketCost ¹ | ✓ | ✓ | ✓ | | | Expenses ¹ | ✓ | ✓ | ✓ | | | Attachment ² | ✓ | ✓ | | ✓ | | Account | ✓ | | | | | AccountLocation | ✓ | | | | | Resource | ✓ | | | | ¹ Required only if expense or charge sync is enabled. ² Required only if attachment sync is enabled. --- ## Your reference sheet Non-secret values entered above are saved here for easy access; the API user password is masked and never saved — enter it directly into the connector. Use **Copy all** to export before clearing browser data. --- ### Overview URL: /docs/connectors/platforms/autotask/overview The Field Nation Autotask connector links your Autotask PSA instance to Field Nation, keeping work orders, time entries, expenses/charges, notes, and attachments synchronized between both platforms. When work happens in Field Nation, it flows into Autotask automatically. When you need to dispatch against an existing Autotask ticket, import it directly into Field Nation — no re-entry required. Field Nation events push data to Autotask in near real time. Autotask tickets import into Field Nation on demand. Work orders, time entries, expenses/charges, notes, and attachments — including User-Defined Fields API access is available on Autotask Pro and Enterprise editions. Essentials does not include API access. Outbound sync requires no webhook configuration. Autotask webhooks can optionally automate inbound imports. --- ## Architecture The connector sits between Field Nation and Autotask PSA. Field Nation is always the initiating side — it either fires an outbound event (provider logs time, submits an expense) or an admin triggers an inbound import by providing an Autotask ticket ID. Outbound sync requires no webhook configuration; Autotask webhooks are optional and only used to automate inbound imports (see [Workflow Setup](/docs/connectors/platforms/autotask/workflow#automated-inbound--configure-autotask-webhooks)). ```mermaid --- config: flowchart: curve: linear --- flowchart LR subgraph FN["Field Nation"] E1([Work order events]) E2([Admin import request]) end subgraph CONN["FN–Autotask Connector"] direction TB OB["Outbound\nevent-driven"] IB["Inbound\non-demand"] end subgraph AT["Autotask PSA"] T([Tickets]) D([Time / Expenses / Attachments]) end E1 -->|"Work order event"| OB OB -->|"Field Nation API"| T OB --> D E2 --> IB T -->|"Ticket data + UDFs"| IB IB -->|"Field mappings applied"| FN ``` --- ## How sync works ![Setup journey — 4 stages from prerequisites to validation: Prerequisites (~10 min), Configure Autotask (~40 min), Configure Field Nation (~20 min), Test and Validate (~15 min)](./images/setup-journey.webp) **Outbound (Field Nation → Autotask):** When a provider logs time, submits an expense, adds a note, or uploads a file in Field Nation, the connector writes that data to the linked Autotask ticket automatically. **Inbound (Autotask → Field Nation):** A Field Nation admin provides an Autotask ticket ID. The connector fetches the ticket — including standard fields, account and contact data, and any User-Defined Fields — and creates or updates the Field Nation work order. ```mermaid sequenceDiagram participant FN as Field Nation participant C as Connector participant AT as Autotask PSA Note over FN,AT: Outbound — Field Nation → Autotask FN->>C: Work order event fires C->>AT: REST / SOAP API call AT-->>C: Record ID confirmed C-->>FN: Sync complete Note over FN,AT: Inbound — Autotask → Field Nation FN->>C: Admin imports ticket ID C->>AT: Fetch ticket + UDFs AT-->>C: Ticket data C->>C: Apply field mappings C-->>FN: Work order created or updated ``` --- ## Use cases **Scenario:** Your team manages all customer work in Autotask. When a ticket requires a field technician, you want to dispatch through Field Nation without re-entering ticket details. 1. A ticket is created in Autotask (by your team or automatically). 2. A Field Nation admin opens a new work order and selects **Import from Autotask**. 3. The connector fetches the ticket — title, description, account, location, and any UDFs — and populates the work order. 4. The work order is posted to Field Nation's marketplace and assigned to a provider. 5. As work progresses, time entries, expenses/charges, notes, and attachments flow back to the Autotask ticket automatically. **Scenario:** Providers complete work orders in Field Nation. You need all labor hours, expenses/charges, and receipts to appear in Autotask for billing and reporting — without manual entry. - **Time entries:** When a provider submits a time entry, the connector writes it to the linked Autotask ticket with start time, end time, and hours worked. - **⚠️ Time entry deletions:** When a provider removes a time entry in Field Nation, **the corresponding record is automatically deleted in Autotask.** This cannot be undone — review time entries before removing them. - **Expenses/charges:** When a provider submits an expense or charge, it routes to either the Autotask **Expense module** (default) or as a **Ticket Cost** under Service Desk — one destination per connector, configurable in settings. - No manual export or CSV upload is required. **Scenario:** Your operations team wants end-to-end automation — tickets created in Autotask dispatch field work automatically, and all field activity syncs back without any manual steps. Enable **Outbound Create** in the connector settings. With this on: - Every new Field Nation work order creates a new Autotask ticket automatically. - The returned ticket ID is stored on the work order for all future sync events. - Time entries, expenses/charges, notes, and attachments flow to Autotask as work happens. - Inbound import is still available on demand for tickets that originated in Autotask. This mode is best suited for teams where Field Nation is the primary dispatch tool and Autotask is the system of record. --- ## Configuration stages ### Verify Autotask edition and API access Confirm your Autotask subscription includes API access (Pro or Enterprise). Check that **API User** appears as a Resource Type under Admin → Resources (Users). If it does not, contact your Autotask administrator. ### Create a dedicated security level and API user In Autotask Admin, create a new Security Level with **API User** as the license type and grant the minimum required permissions for tickets, time entries, and any optional features (expenses, attachments). Then create an API user assigned to that security level. Use a service account email, not a personal one. ### Connect the Field Nation connector Log in to Field Nation as a Company Admin and navigate to **Integrations → Field Services → Autotask**. Enter the API username, password, and integration code (leave blank for the Field Nation default). The connector discovers your Autotask zone automatically from the credentials. ### Validate and configure optional features **Save** the connector configuration — Field Nation automatically tests the connection on save. If credentials are valid, the connector shows as active. Then enable optional features as needed: Outbound Create, expense routing destination, and attachment sync. See the [Configuration guide](/docs/connectors/platforms/autotask/configuration) for the full step-by-step details. --- ## Supported operations | Data type | Create | Read | Update | Delete | |---|:---:|:---:|:---:|:---:| | Ticket | ✓ | ✓ | ✓ | | | Time Entry | ✓ | ✓ | ✓ | ✓ | | Ticket Note | ✓ | | | | | Ticket Cost (charge) | ✓ | | ✓ | | | Expense Report / Item | ✓ | | | | | Attachment | ✓ | ✓ | | ✓ | | Account | | ✓ | | | | Account Location | | ✓ | | | | Resource | | ✓ | | | > [INFO] Ticket Delete is not supported by the Autotask REST API. Expense edits route through ExpenseReport — individual ExpenseItem updates are not exposed via the connector. --- ## Before you begin Complete the checks below before starting the [Configuration](/docs/connectors/platforms/autotask/configuration) steps. The Autotask side (Steps 1–5 of Configuration) must be completed by an **Autotask administrator** and can be handed off to them independently. Use the checklist below to track progress across both environments. ### Autotask requirements #### 1. Edition and API access API access is available on **Autotask Pro and Enterprise** editions. Essentials-tier subscriptions do not include API access — contact your Autotask administrator to confirm your entitlement. Use one of these methods to confirm: | Method | Where to look | What to confirm | |---|---|---| | Check for API User license | Admin → Resources (Users) → API User | **API User** appears as a Resource Type | | Check Security Levels | Admin → Security Levels → New Security Level | **API User** is selectable as the license type | | Subscription confirmation | Admin → Features & Settings | Edition is listed as Pro or Enterprise | If you cannot locate these options, ask your Autotask administrator: *"Does our Autotask instance include API User licenses?"* #### 2. Zone and network access Autotask routes API calls to a regional zone (for example, `webservices2.autotask.net` or `webservices4.autotask.net`). The connector auto-discovers the correct zone from your credentials — no manual entry is required. If your network restricts outbound HTTPS traffic, allow `*.autotask.net` on port 443. To confirm your zone is reachable before setup: ```sh curl -s "https://webservices.autotask.net/atservicesrest/v1.0/zoneInformation?user=YOUR_AUTOTASK_USERNAME" ``` Expected result: a JSON response with `zoneName`, `url`, `webUrl`, and `ci` fields. | Symptom | Likely cause | Resolution | |---|---|---| | 404 or empty response | Username doesn't exist in Autotask | Use a valid Autotask login username | | 401 or authorization error | API access disabled or username incorrect | Confirm API entitlement with your Autotask administrator | | Timeout or connection error | Network egress restriction | Allow outbound HTTPS to `webservices.autotask.net` and your zone base URL | #### 3. Integration code Field Nation provides a registered partner integration code by default. **No action is required for most customers** — leave the Integration Code field blank during configuration and the Field Nation default is used automatically. You only need to act if your organization requires a **customer-specific integration code**: obtain it from Autotask Support, then enter it during [Step 4 of Configuration](/docs/connectors/platforms/autotask/configuration#step-4-determine-your-integration-code). ### Field Nation requirements | Requirement | Details | |---|---| | Company Admin access | Required to navigate to **Integrations → Marketplace** and save connector settings | | Target environment identified | Configure sandbox (`ui-sandbox.fndev.net`) before production (`app.fieldnation.com`) | | API credentials ready | Autotask API username, password, and integration code (if applicable) from Steps 1–4 | --- ## What syncs ### Outbound (Field Nation → Autotask) | Field Nation event | Autotask entity written | |---|---| | Work order created (Outbound Create enabled) | New Ticket | | Work order updated | Ticket fields updated | | Time entry logged | TimeEntry created or updated | | Time entry deleted | TimeEntry deleted | | Expense/charge submitted | ExpenseReport + ExpenseItem **or** TicketCost — one destination, configurable in connector settings | | Note added to work order | TicketNote | | File attached | Attachment (embedded file or URL link depending on size) | | Signature captured | Attachment (embedded file, always public visibility) | ### Inbound (Autotask → Field Nation) | Data imported from Autotask | Notes | |---|---| | Ticket fields | Title, description, status, priority, due date | | Account and contact | Company name, location, contact information | | User-Defined Fields | Mapped based on your inbound field configuration | | Attachments | Only if attachment import is enabled for your account | > [INFO] The primary sync entity is the Autotask **Ticket**. Service Calls and Projects are also supported as the Service Object Name — confirm with your Field Nation account team which entity type is appropriate for your workflow. --- ## Limitations - **Outbound sync requires no webhook setup.** All outbound events are initiated automatically when work happens in Field Nation. Autotask webhooks are optional — they can automate inbound imports, but are not required for outbound sync to work. - **Ticket entity is primary.** The connector syncs against Autotask Tickets by default. Service Calls and Projects are supported but must be confirmed with your Field Nation account team. Accounts and Contacts are read-only reference data used for lookups. - **Attachment size.** Files over 5 MB sync as URL attachments (link only). Executable and archive file types are blocked at the Autotask API level and always sync as URL links, regardless of size. - **UDF discovery is connection-time.** User-Defined Fields are discovered when the connector first authenticates. If you add UDFs to Autotask after the connector is set up, re-save the connector configuration in Field Nation to pick them up. - **Time zone.** Time entries are sent in UTC by default. Contact your Field Nation account team if you need EST conversion enabled. --- ## Next steps Your Autotask administrator should work through the [Configuration](/docs/connectors/platforms/autotask/configuration) guide to create the required security level and API user, then connect the Field Nation connector. Once configured, see [Workflow Setup](/docs/connectors/platforms/autotask/workflow) to enable and tune sync behaviors for your team. --- ### Workflow URL: /docs/connectors/platforms/autotask/workflow The Autotask connector supports two independent sync directions. Each is configured separately and can be enabled independently based on your team's workflow. | Direction | How it works | Requires | |---|---|---| | **Outbound** (FN → Autotask) | Field Nation events automatically push data to Autotask in near real time | Connector configured in [Configuration Step 6](/docs/connectors/platforms/autotask/configuration#step-6-configure-the-field-nation-connector) | | **Inbound** (Autotask → FN) | Autotask tickets are imported into Field Nation — manually or via webhook | Connector configured in [Configuration Step 6](/docs/connectors/platforms/autotask/configuration#step-6-configure-the-field-nation-connector). Automated: webhook configured in Autotask | --- ## Outbound workflows (Field Nation → Autotask) ### Work order create and update When a Field Nation work order is created or updated, the connector can create or update the corresponding Autotask ticket. **Outbound Create** controls whether new work orders automatically generate Autotask tickets: | Setting | Behavior | |---|---| | **Disabled** (default) | Sync only updates existing Autotask tickets that were linked during import | | **Enabled** | Every new Field Nation work order creates a new Autotask ticket automatically | Enable in **Field Nation → Integrations → Field Services → Autotask → Manage → Settings → Outbound Create** (toggle). > When Outbound Create is enabled, Autotask requires six fields on every ticket creation: **Title**, **Status**, **Priority**, **AccountID**, **QueueID** (or AssignedResourceID + AssignedResourceRoleID), and **DueDateTime**. These must be configured as outbound field mappings before enabling this feature — missing any one will cause ticket creation to fail silently. See [Required fields for Outbound Create](/docs/connectors/platforms/autotask/configuration#required-fields-for-outbound-create) in the Configuration guide. --- ### Time entry sync When a provider logs time on a Field Nation work order, the connector writes a **TimeEntry** record to the linked Autotask ticket. For full configuration details, see the [Time Log Sync](/docs/connectors/platforms/autotask/guides/time-log-sync) guide. | Field Nation | Autotask TimeEntry field | |---|---| | Start time | `StartDateTime` | | End time | `EndDateTime` | | Hours worked | `HoursWorked` | | Resource ID | `ResourceID` | | Work type | `AllocationCodeID` | | Contract | `ContractID` | | Notes | `SummaryNotes` or `InternalNotes` | **Duplicate prevention:** To prevent duplicates on re-sync, the connector stores the Field Nation time entry ID inside the Autotask time entry record. | Storage setting | Where the tracking ID appears | |---|---| | **Summary Notes** (default) | Appended as `(#)` to Summary Notes | | **Internal Notes** | Appended as `(#)` to Internal Notes | Set in **Field Nation → Integrations → Field Services → Autotask → Manage → Time Logs**. **Deletions:** When a provider's time entry is removed in Field Nation, the connector deletes the corresponding record in Autotask by matching the stored tracking ID. --- ### Expenses/charges sync | Connector setting | Destination in Autotask | Entity written | |---|---|---| | Blank (default) | Expense module | ExpenseReport + ExpenseItem | | `charge` | Service Desk | TicketCost | Configure in **Field Nation → Integrations → Field Services → Autotask → Manage → [Additional Charges](/docs/connectors/platforms/autotask/guides/charge-sync)**. For full field mapping details and setup instructions, see the [Additional Charge Sync](/docs/connectors/platforms/autotask/guides/charge-sync) guide. --- ### Attachment sync | Condition | Autotask attachment type | |---|---| | File ≤ 5 MB | `FILE_ATTACHMENT` (embedded) | | File > 5 MB | `URL` (link only) | | Executable or archive file type | `URL` (Autotask API restriction) | | Signature capture | `FILE_ATTACHMENT`, always public visibility | Attachment visibility (public vs. internal) is configurable globally in connector settings. Signature attachments are always public. Contact your Field Nation account team to enable attachment sync for your account. --- ### Ticket notes Notes added to a Field Nation work order are posted as **TicketNotes** in Autotask. Internal notes can be configured to be visible to resources only, not contacts. --- ## Inbound workflow (Autotask → Field Nation) Inbound imports can be triggered in two ways: - **Manual** — A Field Nation admin provides an Autotask ticket ID on demand - **Automated** — Autotask POSTs a ticket ID to your Field Nation trigger URL when conditions are met --- ### Manual import ### Open work order creation in Field Nation Navigate to your Field Nation account and begin creating a new work order, or open an existing one. ### Select "Import from Autotask" In the integration options, choose to import from Autotask and enter the Autotask ticket ID or number. ### Connector fetches ticket data The connector authenticates to Autotask and retrieves: - Standard fields (title, description, status, priority, due date) - Account and location details - Assigned resource and contact information - User-Defined Fields (UDFs) from your inbound field mappings - Attachments (if attachment import is enabled) ### Review and confirm Imported data populates the work order fields based on your inbound field mappings. Review and save. > [INFO] To enable attachment import for your connector, contact your Field Nation account team. This is controlled by a per-account setting. --- ### Automated inbound — configure Autotask webhooks To have Autotask push tickets to Field Nation automatically, you must configure two elements in Autotask: an **Extension Callout** (the webhook destination) and a **Workflow Rule** (the trigger condition). **Before you start:** Complete [Step 7 of Configuration](/docs/connectors/platforms/autotask/configuration#step-7-get-your-trigger-url) to get your trigger URL. ### Create the Extension Callout In Autotask, go to **Admin → Extensions & Integrations → Other Extensions & Tools → Extension Callout (Tickets)**. Click **New** and fill in the fields: | Field | Value | |---|---| | Name | A descriptive name, e.g. `DispatchToFN` | | Active | ✓ Checked | | URL | Your Field Nation trigger URL from [Configuration Step 7](/docs/connectors/platforms/autotask/configuration#step-7-get-your-trigger-url) | | Username | Your Autotask API username | | Password | Your Autotask API user password | | Ticket User-Defined Field | Leave blank | | HTTP Transport Method | `POST` | | Data Format | `Name Value Pair` | > **Leave Ticket User-Defined Field blank** unless you intentionally want to restrict the callout to tickets that have a specific UDF value. Setting this field is a common reason initial tests fail — the callout will not fire at all if the test ticket does not have a value in the selected UDF. > [INFO] If the Extension Callout has difficulty connecting, Autotask will automatically retry every minute for the next 10 minutes. Click **Save & Close**. ### Create a Workflow Rule Go to **Admin → Automation → Workflow Rules**. Click **New** and ensure the **Entity** is set to **Ticket (Service Desk)**. Name your rule (e.g. `Autotask > FN`). ### Define trigger events and conditions **Events:** Check both **Created by** and **Edited by** (leave the dropdowns set to "Anyone"). This ensures the rule is evaluated whenever a ticket is created or updated. **Conditions:** Define when the ticket is ready to dispatch. For example: | Condition | Operator | Value | |---|---|---| | Status | Equal to | `Dispatch to Field` (or your equivalent status) | > [INFO] Start with a single status condition. Add additional filters (Queue, Issue Type, custom UDF) only if your ticket volume requires further targeting. **UDF duplicate prevention (recommended):** To prevent the same ticket from dispatching twice, add a second condition using a custom UDF (e.g. `FN Sync Status`): | Condition | Operator | Value | |---|---|---| | `FN Sync Status` UDF | does not equal | `Sent` | After the first successful dispatch, update this UDF to `Sent` via an outbound field mapping so subsequent ticket updates do not re-trigger an import. ### Link the Extension Callout Scroll to the **Actions** section at the bottom of the Workflow Rule. > [INFO] Leave the **Updates** section blank unless you explicitly want Autotask to automatically modify fields on the ticket when the rule fires. Find the **Then Execute Extension Callout** dropdown and select the Extension Callout you created in Step 1 (e.g. `DispatchToFN`). Click **Save & Close**. ### Test the integration 1. Open an existing test ticket in Autotask. 2. Change the ticket's status to match your Workflow Rule condition (e.g. `Dispatch to Field`) and save. 3. Wait approximately one minute. 4. Check your Field Nation account — a new work order should have been created from the ticket. --- ### Configure what data to load When a webhook fires (or a manual import is initiated), the connector fetches data from Autotask in two passes: 1. **Primary entity fetch** — Full ticket (or ServiceCall/Project) including all standard fields 2. **Related entity fetch** — Account, Location, Contact, Resource, and any UDFs mapped in your inbound field mappings Control what gets loaded by configuring your inbound field mappings in **Step 9 of Configuration**. #### Standard fields always loaded | Autotask entity | Fields fetched | |---|---| | Ticket | Title, Description, Status, Priority, DueDateTime, CreateDateTime, CompletedDate | | Account | AccountName, Phone, Address fields | | Account Location | Address, City, State, PostalCode, Country | | Contact | FirstName, LastName, Email, Phone | | Resource | FirstName, LastName, Email | #### UDF loading UDFs are only fetched if they are included in your inbound field mappings. After adding new UDFs to Autotask: 1. Click **Refresh Fields** in the connector settings. 2. Add the new UDF to your inbound field mappings. 3. Save — the UDF will be fetched on all subsequent imports. #### Attachments Attachments are **not** loaded by default. To enable: - Contact your Field Nation SE to enable attachment import for your account. - Once enabled, all attachments on the Autotask ticket are fetched and added to the work order. --- ## Workflow diagrams ### Outbound (Field Nation → Autotask) ```mermaid --- config: flowchart: curve: linear --- flowchart LR E([Work order event]) --> B{Event type} B -->|Create / Update| C[Ticket created or updated] B -->|Time entry logged| D[TimeEntry written] B -->|Time entry deleted| DEL[TimeEntry deleted] B -->|Expense/charge| F{Routing setting} F -->|Default| G[ExpenseReport + ExpenseItem] F -->|charge| H[TicketCost] B -->|Attachment| I[FILE_ATTACHMENT or URL] B -->|Signature| J[FILE_ATTACHMENT — public] B -->|Note| K[TicketNote created] ``` ### Inbound (Autotask → Field Nation) ```mermaid --- config: flowchart: curve: linear --- flowchart LR W([Autotask webhook]) -->|POST ticket ID| C A([Admin manual import]) -->|Ticket ID| C C[Connector fetches ticket] --> D[Fetch related entities + UDFs] D --> E[Apply inbound field mappings] E --> F([Work order created or updated]) ``` --- ## Troubleshooting **Check:** - Confirm the integration is active in Field Nation settings - Verify credentials are still valid — re-save the connector settings; Field Nation tests the connection automatically on save - Confirm the API user's password has not changed or expired in Autotask - Check the notification email address for sync failure alerts **Check:** - Confirm **Outbound Create** is enabled in connector settings - Verify the API user has **Tickets — Add** permission in Autotask (Step 2 of Configuration) - Confirm **Service Object Name** is set to `ticket` in connector settings - Review sync failure notification emails for the specific error **Cause:** The Time Log ID Storage Location setting changed, causing the connector to lose the link between existing Autotask time entries and Field Nation time entries. **Resolve:** 1. Check the current **Time Log ID Storage Location** setting in Field Nation 2. Confirm existing Autotask time entries contain the tracking ID (formatted as `(#)`) in the expected notes field 3. If the ID is in the wrong field, update the setting and contact Field Nation support for a re-sync **Check:** - Confirm **[Sync Additional Charges to](/docs/connectors/platforms/autotask/guides/charge-sync)** matches your intended destination (`charge` for Ticket Costs, blank for Expense Reports) - Verify the API user has the correct permissions for the chosen destination (Ticket Costs or Expense module) - Confirm the Field Nation expense has been submitted, not just saved as a draft **Check:** - Confirm the webhook payload contains the ticket ID in the `id` field - Verify the trigger URL in Autotask matches the one from Step 7 of Configuration — tokens are unique per account - Check the Autotask Webhook Delivery Log — a success response confirms Field Nation received the request; an error response means the delivery failed - Confirm the ticket meets your inbound field mapping requirements (required fields are mapped) **Cause:** Webhook trigger conditions are firing on every update, not just the initial dispatch event. **Resolve:** - Add a UDF-based duplicate prevention condition to the webhook (see [Trigger conditions](#trigger-conditions)) - Create an outbound mapping that sets the `FN Sync Status` UDF to `Sent` after the first successful import **Check:** - Confirm you are providing the Autotask ticket **ID** (numeric), not the ticket number or display ID - Verify the API user has **Tickets — View** permission - Confirm the ticket exists and is not archived or deleted in Autotask **Expected behavior:** Files over 5 MB, and certain file types (executables, archives), always sync as URL attachments — this is an Autotask API restriction, not a connector limitation. To reduce URL-only attachments, ensure files uploaded to Field Nation are under 5 MB where possible. --- ### Configuration URL: /docs/connectors/platforms/connectwise/configuration Setup has two owners. The **ConnectWise administrator** completes the ConnectWise-side work (Steps 1–4) and can do it independently. Once they hand off the credentials, the **Field Nation Integration Owner** completes the Field Nation-side work (Steps 5–8). Complete the steps in order. **Estimated setup time:** 30–60 minutes for a ConnectWise administrator familiar with roles and API keys. > [INFO] Before starting, confirm your ConnectWise license includes API member support and that you have identified a Template Ticket ID — an active, accessible service ticket used for field discovery. See [Before you begin](/docs/connectors/platforms/connectwise/overview#before-you-begin) on the Overview page. --- ## Step 1: Confirm environment and scope **Who:** ConnectWise Admin Before creating any accounts or keys, make these four decisions. You can answer all of them yourself — no call required. - **Environment** — Pick **sandbox** to test safely first, or **production** to go live. - **Scope** — List the ConnectWise **service boards** and **ticket types** you want to send to Field Nation. Only these will sync. - **ConnectWise host URL** — Your instance's hostname, for example `api-na.myconnectwise.net`. - **Optional features** — Decide which you'll use: **auto-dispatch**, **attachment sync**, **message sync**, **project ticket linking**, and **schedule sync**. Each adds specific permissions in Step 3, so choosing now keeps your security role scoped to exactly what you use. Attachment sync also requires the **Client Documents** contract feature; schedule sync is driven by field mapping rather than a toggle (see Steps 6–7). **Why this step matters:** Confirming scope before provisioning prevents over-granting access and avoids accidental changes to boards or ticket types that are not part of the integration. > [INFO] **If unsure about any choice**, your Field Nation Solutions Engineer can help — but it's not required to proceed. --- ## Step 2: Create a dedicated API member **Who:** ConnectWise Admin **Navigation:** System → Members → API Members (tab) → New Member Create a new member specifically for the Field Nation integration. Do not reuse an existing employee account or admin member. | Field | Value | |---|---| | Member Type | **API Member** | | Member ID | `svc-fieldnation` (or a similar service account name) | | First Name / Last Name | Field Nation / Integration | | Email | A monitored alias — e.g. `integrations@yourdomain.com` | | Default Territory | Your primary territory | | Security Role | Field Nation Integration (created in Step 3) | | Status | **Active** | **Why a dedicated member:** A dedicated account keeps sync actions attributed to the integration rather than a person, allows API key rotation without disrupting anyone's access, and limits the blast radius if keys are compromised. --- ## Step 3: Create a security role and assign minimum permissions **Who:** ConnectWise Admin **Navigation:** System → Security Roles → New Role **Role name:** `Field Nation Integration` > **Do not assign the Administrator role or any role with billing, invoicing, or user management permissions.** Grant only what is listed below. Leave all other permissions at their defaults (no access). ### Required permissions Grant only what is listed below. Leave every other permission at its default (no access). The Module / Area names below match ConnectWise's actual security-role structure — they are not generic placeholders. | Module / Area | Permission | Required | Notes | |---|---|---|---| | **Service Desk** → Service Tickets | Inquire, Add, Edit (set to **All**) | Always | Core sync — read existing tickets, create from Field Nation, update status. **Also covers Ticket Notes sync** — there is no separate Ticket Notes permission to grant. _**Without this permission, the connector cannot read tickets, create work orders from ConnectWise, update ticket status, or sync ticket notes.**_ | | **System** → Table Setup | Inquire, Add (set to **All**) | Always | Required for Custom Fields and Note Types. Click the **(customize)** link next to Table Setup and move **General / Custom Fields** and **Service / Note Type** into the **Allow** column. | | **Companies** → Company Maintenance | Inquire **All**; add **Add / Edit** if an outbound company mapping is configured | Always (Add/Edit conditional) | Map work orders to the correct ConnectWise company and associate contacts. An outbound **company** mapping writes back to ConnectWise — creating or updating the company — which requires Add/Edit. | | **Companies** → Configurations | Inquire **All** | Always | Map work location / site for work orders. | | **Companies** → Manage Attachments | Inquire **All** (always); add **Add: All** for attachment export | Always (Inquire) | Required for every fetch — missing Inquire causes a `403` on save, before any sync runs. Add **Add: All** only to enable attachment export. | | **Project** → Project Tickets | Inquire **All** | Project tickets only | Link service tickets to projects. | | **Service Desk** (or Time & Expense) → Resource Scheduling | Inquire, Add, Edit (set to **All**) | If schedule fields are mapped | Required when you map `schedule.dateStart` / `schedule.dateEnd`. | > [INFO] **Ticket Notes and Custom Fields are not separate top-level permissions in ConnectWise.** Ticket Note sync rides on the Service Tickets permission, and Custom Fields / Note Types are exposed through **System → Table Setup** using the **(customize)** link. Granting Table Setup without customizing it will leave custom fields undiscoverable during Refresh Fields. > **ConnectWise caches role permissions per session.** After changing a security role, log out and back in (and, for API members, the change may take a minute to apply) before re-testing — otherwise the old permissions persist and you'll chase a phantom error. ### Permissions to explicitly exclude | Module / Area | Grant access? | |---|---| | Finance (Billing / Invoicing) | No | | System → User Management | No | | System Administration / Admin-level access | No | | HR / Payroll | No | | Procurement / Purchasing | No | If your organization uses a base security role that includes any excluded modules, create a new role from scratch rather than modifying an existing one. --- ## Step 4: Generate API keys **Who:** ConnectWise Admin **Navigation:** System → Members → [select the API member from Step 2] → API Keys tab → Add (+) ![ConnectWise API Member detail — Member ID, Role ID, and API Keys tab](./images/connectwise_user.webp) Follow these steps in order. **Do not close the dialog until both keys are saved.** 1. Click the **+** icon to generate a new key pair 2. Enter a description: `Field Nation Integration` 3. Copy the **Public Key** — retrievable from the member record later if needed 4. Copy the **Private Key** — **it will not be shown again after you close this dialog** 5. Store both keys immediately in a password manager or secrets vault 6. Note the **Member ID** from the member record for your own records — the connector's **User Name** field accepts it, but the field is not read or used by the connector in any way 7. Confirm your **Company** identifier — this is your **login company** (see the callout below on exactly which value this is and where to find it) > **Company is your login identifier** — the value in the **Company** field on your ConnectWise sign-in screen. Three ways to confirm it: 1. Sign in to ConnectWise — the **Company** field on the login screen is the value. 2. Open any company ticket → click **Share** → the URL contains `companyName=`. 3. **System → My Company Profile → Miscellaneous Options → Customer Portal URL Override** — the identifier appears after the trailing `/` in the URL. A `401 Invalid Token` with otherwise correct keys almost always means Company is wrong. > [INFO] **User Name and Password are not used for authentication or any other function.** Only Company, Public Key, and Private Key are read by the connector. You can leave both fields blank or enter any value — they have no effect. ### Credentials to collect Gather all values below before proceeding to Step 5. ![ConnectWise Template Ticket — its ID drives field discovery](./images/connectwise_template.webp) **Private Key** — Enter directly in the Field Nation connector settings. Do not store it here. **Password** — Leave blank. The connector form renders a Password field, but key-pair authentication does not use it. --- ## Step 5: Configure the Field Nation connector **Who:** Field Nation Integration Owner **Navigation:** Field Nation → Integrations → Field Services → ConnectWise → Manage → Settings Enter the values collected in Step 4. Use the hostname only — no `https://` prefix and no trailing slash. ![Field Nation connector settings form — credential and trigger fields](./images/connectwise_settings.webp) The labels below match exactly what the connector form shows. User Name and Password appear in the form but are not used — leave them blank. | Field (UI label) | Value | |---|---| | ConnectWise Host | Hostname only — e.g. `api-na.myconnectwise.net` | | Company | Your **login** company identifier (e.g. `acme_corp`) — see the Company tip in Step 4 for three ways to confirm the correct value | | User Name | **Not used.** The connector does not read this field. Leave blank or enter any value. | | Password | **Not used.** Leave blank. | | Public Key | From Step 4 | | Private Key | From Step 4 — entered directly | | Template Ticket ID | Numeric ID of an active ticket for field discovery (cannot have any trailing spaces) | | Trigger On Status | The exact ConnectWise ticket status text that must be present for the connector to create a work order (e.g. `Dispatch to FN`). Case-sensitive. Tickets whose current status does not match are silently skipped — no error is surfaced. **Set this before enabling sync.** | | Notification Email Addresses | Comma-separated email addresses to receive alerts when a sync transaction fails. Optional. | | Outbound Create | Toggle — create a ConnectWise ticket for every new Field Nation work order (see Step 7) | | Fetch All Notes | Toggle — retrieve all ConnectWise note types, not just ticket notes (see Step 7) | After saving, click **Refresh Fields**. The connector authenticates to ConnectWise using the Template Ticket ID and retrieves standard fields, related entity fields (company, contact, site), and any custom fields on that ticket type. > If you add new custom fields to ConnectWise after the connector is configured, click **Refresh Fields** again to make them available for mapping. Fields are discovered at connection time only. --- ## Step 6: Configure field and status mappings **Who:** Field Nation Integration Owner **Navigation:** Field Nation → Integrations → Field Services → ConnectWise → Field Mappings Configure mappings for both directions based on the scope decisions from Step 1. ### Inbound (ConnectWise → Field Nation) Runs when a ConnectWise callback fires and a ticket is imported into Field Nation. | ConnectWise field | Field Nation field | Notes | |---|---|---| | `summary` | `title` | Direct copy | | `initialDescription` | `description` | Initial description text | | `status.name` | `status.name` | Use Status Mapping | | `site.addressLine1` | `location.address1` | Site address | | `site.city` | `location.city` | Site city | | `customFields[{id}]` | Custom field | Discovered via Refresh Fields | ### Mapping direction (the arrows) Each mapping row has two direction toggles between the Field Nation and ConnectWise columns: **← (inbound, CW → FN)** and **→ (outbound, FN → CW)**. A row can be inbound-only, outbound-only, or both. This direction control is the key to a clean outbound sync — see [Outbound workflows](/docs/connectors/platforms/connectwise/workflow). ### Outbound (Field Nation → ConnectWise) Runs when a Field Nation work order event fires. | Field Nation field | ConnectWise field | Notes | |---|---|---| | Work order title | `summary` | **Required** to create a ticket | | Buyer | `company` (name) | **Required** to create a ticket — the connector looks up the company by name. The value must match an existing ConnectWise company name exactly. | | _(static)_ | `board` (id) | **Required** to create a ticket — the board new tickets land on | | Work order status | `status.name` | Via Status Mapping | Refresh Fields also exposes additional inbound-mappable fields from the ConnectWise ticket — including **Initial Internal Analysis** (internal notes field) and **Initial Resolution** (resolution notes field) — which you can map to Field Nation fields that fit your workflow. ### Status mapping Status mapping translates ConnectWise status values to Field Nation status IDs. Configure under **Field Mappings → Status**. The status values in the dropdown are populated after Refresh Fields runs. > **Status values are unique to your ConnectWise instance.** Never copy mappings from another organization's configuration. Always use the values discovered by Refresh Fields from your own instance. --- ## Step 7: Enable optional features ### Outbound Create Enable to have every new Field Nation work order automatically create a ConnectWise service ticket. By default the connector only syncs to tickets already linked via a callback. **Field Nation → Integrations → Field Services → ConnectWise → Manage → Settings → Outbound Create** (toggle) Requires **Service Tickets — Add** permission on the security role. ### Auto-dispatch When enabled, work orders the connector creates from ConnectWise tickets are dispatched to providers automatically rather than held as drafts. Auto-dispatch is gated by the **auto-dispatch** contract entitlement — contact Field Nation Support to enable it, then turn on the auto-dispatch setting in the connector configuration. ### Message sync Syncs messages and notes between Field Nation work orders and ConnectWise Ticket Notes. Message sync depends on the **Messages** contract entitlement — a platform-level feature, not a switch in the connector form. If it's unavailable, contact Field Nation Support to confirm the entitlement. Notes are written to the ConnectWise **Internal Analysis** field (internal — visible only to your team) or the **Detail Description** field (external — visible to the ticket's contacts) depending on message type. ### Attachment sync Syncs files between Field Nation work orders and ConnectWise Documents. The two toggles live in the **Client Documents** mapping section (labels exactly as the form shows them): - **"Import all files from Connectwise ticket to Client Documents when a Work Order is created"** — inbound (CW → FN). Note the timing: it imports **at work-order creation**, so the file must already be on the ConnectWise ticket *before* it's sent to Field Nation. Files added later won't be pulled retroactively. - **"Send provider uploads to Connectwise as Document"** — outbound (FN → CW). Fires when a **provider** uploads a file to the work order (buyer-side Client Documents uploads do not trigger it). Documents are always created as **private** in ConnectWise — this is hardcoded and not configurable. The **Deliverables → Access Control** dropdown only controls inbound visibility (files synced from ConnectWise into Field Nation), not this direction. Attachment sync also requires the **Client Documents** contract feature; if the toggles don't appear, contact [Field Nation Support](https://app.fieldnation.com/support-cases) to enable that entitlement. Requires **Companies → Manage Attachments** on the security role — **Inquire** for inbound import and **Add** for outbound export. The connector de-duplicates outbound uploads by a fingerprint hash, so re-uploading the same file is skipped. > [INFO] **Attaching a file to a ticket in the ConnectWise UI** (e.g. to test inbound import): the ticket must be **saved first** — the Attachments area only appears on an existing ticket, behind a paperclip icon, not a left-nav section. And it's gated by the logged-in user's role (Manage Attachments) — if you don't see it, that user's role lacks the permission, or you need to log out/in after granting it. ### Fetch All Notes When enabled, retrieves all note types from ConnectWise — standard ticket notes, time entry notes, and meeting notes — rather than ticket notes only. ### Notification Email Addresses Enter one or more comma-separated email addresses in **Settings → Notification Email Addresses**. The connector sends an alert to these addresses when a sync transaction fails — for example, when a callback fires but the work order cannot be created due to a missing required field or an API error. This is the primary way to detect silent sync failures. Optional, but recommended for production deployments. ### Schedule sync Schedule entries sync automatically whenever you map `schedule.dateStart` / `schedule.dateEnd` (Step 6) — there is **no separate enable toggle**. Entries are always fetched inbound and written outbound based purely on those mappings. > **Test in a sandbox first — here's the self-check.** 1. Map `schedule.dateStart` → work order time window **start**, and `schedule.dateEnd` → **end**. 2. Run one test ticket through in sandbox. 3. **If the dates don't appear** on the ConnectWise schedule entry (or land on the wrong resource), the board's schedule **type** doesn't match the mapping — schedule entries are created with default type and member-assignment values. 4. **Fix:** remove the schedule mapping, correct it to match your board's schedule type, then retry. Repeat until the dates land correctly before relying on it in production. --- ## Step 8: Validate the connection **Who:** Field Nation Integration Owner Before testing, confirm sync is active: **Field Nation → Integrations → Field Services → ConnectWise → Manage → Settings → Enable Sync** (toggle on). The connector will not process any inbound or outbound events until this is enabled. Complete each validation step before declaring the integration active. - [ ] Click **Test Connection** in the connector UI (or trigger a test sync event) and confirm no authentication errors appear in the connector status - [ ] Trigger a test callback from ConnectWise — update a test ticket to meet your callback conditions (see [Callback Setup guide](/docs/connectors/platforms/connectwise/guides/callback-setup)) - [ ] Confirm the work order is created in Field Nation from the ConnectWise ticket - [ ] Update the Field Nation work order status — confirm the ConnectWise ticket reflects the change - [ ] Post a message on the work order — confirm it appears as a Ticket Note in ConnectWise (if message sync enabled) - [ ] If attachment sync is enabled: upload a test file — confirm it appears in ConnectWise Documents - [ ] Check ConnectWise **audit logs** — confirm all sync actions are attributed to the API member, not a human account ### Troubleshooting The **Company** field is likely set to the display name or a customer record instead of the login company identifier. See the Company tip in Step 4 for three ways to confirm the correct value. Re-enter Company, Public Key, and Private Key — check for extra spaces or line breaks. Authentication uses only these three fields; User Name and Password are not used. If the host URL includes `https://` or a trailing slash, remove it. Use hostname only: `api-na.myconnectwise.net`. The **Companies → Manage Attachments — Inquire** permission is missing. The save-time check reads ticket documents and fails without it. Add Inquire: All to the security role, then log out and back in to flush the permission cache before retrying. The Template Ticket ID is invalid, deleted, or inaccessible to the API member. Use an active ticket from the correct service board. If fields are still missing after a valid ticket, confirm the API member's security role includes **System → Table Setup** with Custom Fields and Note Types in the Allow column. The **Trigger On Status** value does not match the ConnectWise status exactly. Confirm the value matches the status name as configured in ConnectWise — casing and spacing must be identical. The API keys have been revoked or expired. Regenerate the key pair in ConnectWise (Step 4) and re-enter both keys in the Field Nation connector settings. Check two things: message sync is enabled in Step 7, and the security role includes **Service Desk → Service Tickets — Inquire, Add, Edit**. Ticket Note sync rides on the Service Tickets permission — there is no separate note permission to grant. Confirm all three conditions are met: - **Companies → Manage Attachments — Inquire** is on the role (required for import) - **Companies → Manage Attachments — Add** is on the role (required for export) - The Client Documents toggles are enabled in Step 7 If the toggles are not visible, the **Client Documents** contract feature is not enabled — contact [Field Nation Support](https://app.fieldnation.com/support-cases) to enable it. A reference or list field (status, type, country, custom-field dropdown) is mapped outbound with a value ConnectWise cannot resolve. Set that field to **inbound-only (←)**, or ensure the mapped value exactly matches a valid ConnectWise option. ConnectWise reports only the first failing field — fix and retry until all fields resolve. A required field for ticket creation is not mapped outbound. ConnectWise requires `company`, `board`, and `summary` on every ticket create — confirm all three are mapped in the outbound (→) direction. Schedule sync is mapping-driven — there is no toggle. If dates are missing, the `schedule.dateStart` / `schedule.dateEnd` fields are either not mapped or the board's schedule type doesn't match the mapping. Remove the schedule mappings, test in sandbox with a confirmed board schedule type, then re-add. [Contact Field Nation Support](https://app.fieldnation.com/support-cases). Include: - The exact error message from the connector or sync log - Your ConnectWise host URL and Field Nation environment (sandbox or production) - Which step you completed last and what you have already tried --- ## Minimum permissions reference | Module / Area | Inquire | Add | Edit | Delete | Required | |---|:---:|:---:|:---:|:---:|---| | Service Desk → Service Tickets | ✓ | ✓ | ✓ | | Always (also covers Ticket Notes) | | System → Table Setup | ✓ | ✓ | | | Always (Custom Fields + Note Types) | | Companies → Company Maintenance | ✓ | ✓¹ | ✓¹ | | Always (¹Add/Edit if an outbound company mapping is configured) | | Companies → Configurations | ✓ | | | | Always | | Companies → Manage Attachments | ✓ | ✓¹ | | | Always (Inquire — connection reads ticket documents); ¹Add for attachment export | | Project → Project Tickets | ✓ | | | | Project tickets only | | Service Desk (or Time & Expense) → Resource Scheduling | ✓ | ✓ | ✓ | | If schedule fields are mapped | | Finance / Billing / Invoicing | | | | | Never | | System → User Management | | | | | Never | | System Administration | | | | | Never | --- ## Your reference sheet --- ### Overview URL: /docs/connectors/platforms/connectwise/overview The Field Nation ConnectWise connector links your ConnectWise Manage instance to Field Nation, keeping service tickets and work orders synchronized between both platforms. When a ConnectWise ticket **transitions into** your configured trigger status, it dispatches a Field Nation work order automatically. When work happens in Field Nation — status changes, notes, attachments — it flows back to the ConnectWise ticket without manual effort. > [INFO] **New to these terms? Start here.** - **PSA** — Professional Services Automation, the category ConnectWise Manage belongs to (your ticketing and service-management system). - **Work order** — the field-service job Field Nation creates from your ConnectWise ticket. - **Callback** — ConnectWise automatically pings Field Nation when a ticket is ready, so there's no manual hand-off. - **Inbound** — ConnectWise → Field Nation. **Outbound** — Field Nation → ConnectWise. - **Bidirectional** — changes flow both ways automatically, keeping the ticket and the work order in sync. ConnectWise callbacks send a ticket ID to Field Nation. Work order events update ConnectWise tickets in near real time. Service tickets, ticket notes, attachments, schedule entries, company, contact, and site data — including custom fields Uses ConnectWise REST API with a dedicated API member and public/private key pair — no OAuth, no shared admin credentials ConnectWise sends the ticket ID to Field Nation the moment trigger conditions are met — no polling required --- ## Architecture The connector sits between Field Nation and ConnectWise Manage. ConnectWise initiates inbound events by posting a ticket ID to Field Nation's inbound endpoint when a callback fires. Field Nation initiates outbound events when work order activity occurs — status changes, notes, attachments — and writes those back to the ConnectWise ticket. ```mermaid --- config: flowchart: curve: linear --- flowchart LR subgraph CW["ConnectWise Manage"] T([Service Tickets]) CB([Callback / Webhook]) end subgraph CONN["FN–ConnectWise Connector"] direction TB IB["Inbound\ncallback-driven"] OB["Outbound\nevent-driven"] end subgraph FN["Field Nation"] WO([Work Orders]) E([Work order events]) end CB -->|"POST ticket ID"| IB T -->|"Ticket + related data"| IB IB -->|"Field mappings applied"| WO E --> OB OB -->|"ConnectWise REST API"| T ``` --- ## How sync works Setup is shared between two owners. The **ConnectWise administrator** prepares the ConnectWise side — API member, security role, keys, and the callback — and the **Field Nation Integration Owner** connects Field Nation, maps fields, enables optional features, and validates end-to-end. The diagram below shows the high-level journey; the [Before you begin](#before-you-begin) checklist breaks it into detailed stages, each labeled with its owner. ![Setup journey — 5 stages: Stage 1 ConnectWise prerequisites (~30 min), Stage 2 Field Nation prerequisites (~5 min), Stage 3 Configure Field Nation connector (~20 min), Stage 4 Configure ConnectWise callback (~20 min), Stage 5 Test and validate (~15 min)](./images/setup-journey.webp) Once configured, the connector runs in two independent directions: **Inbound (ConnectWise → Field Nation):** When a ConnectWise ticket **transitions into** your configured trigger status, ConnectWise posts the ticket ID to Field Nation. (If you want board- or custom-field-based triggering, use a ConnectWise Workflow Rule to set that trigger status when the condition is met.) The connector fetches the full ticket, applies your field mappings, and creates or updates the corresponding Field Nation work order. **Outbound (Field Nation → ConnectWise):** When work order events fire in Field Nation — status updates, notes posted, attachments uploaded — the connector writes those changes back to the linked ConnectWise service ticket via the REST API. ```mermaid sequenceDiagram participant CW as ConnectWise participant C as Connector participant FN as Field Nation Note over CW,FN: Inbound — ConnectWise → Field Nation CW->>C: Callback fires (ticket ID) C->>CW: GET /service/tickets/{id} + related data CW-->>C: Ticket + Company + Contact + Site + Notes C->>C: Apply field mappings C-->>FN: Work order created or updated Note over CW,FN: Outbound — Field Nation → ConnectWise FN->>C: Work order event fires C->>CW: PATCH /service/tickets/{id} C->>CW: POST /service/tickets/{id}/notes CW-->>C: Confirmed C-->>FN: Sync complete ``` --- ## Use cases **Scenario:** Your team manages all buyer work in ConnectWise. When a ticket requires an on-site field technician, you want to dispatch through Field Nation without re-entering ticket details. 1. A service ticket is created or updated in ConnectWise. 2. When the ticket reaches the configured trigger status (for example, "Dispatch to FN"), the callback fires and sends the ticket ID to Field Nation. 3. The connector fetches the ticket — summary, description, company, site, contact, and custom fields — and creates a Field Nation work order. 4. The work order is posted to Field Nation's marketplace and assigned to a provider. 5. As work progresses, status updates and notes flow back to the ConnectWise ticket automatically. **Scenario:** Providers complete work in Field Nation. You need status changes and technician notes to appear in ConnectWise for your service team — without manual copy-paste. - **Status updates:** When a Field Nation work order status changes, the connector updates the ConnectWise ticket status to match your configured mapping. - **Notes:** When a message is posted on a Field Nation work order, the connector creates a Ticket Note in ConnectWise. Notes can be configured as internal (analysis only) or external (detail description visible to contacts). - **Attachments:** When a provider uploads a file on the work order, the connector uploads it to ConnectWise Documents — if attachment sync is enabled. **Scenario:** Your operations team wants end-to-end automation — ConnectWise is the system of record, Field Nation handles dispatch and execution, and both stay in sync throughout the lifecycle. Enable **Outbound Create** in the connector settings. With this on: - Every new Field Nation work order creates a new ConnectWise service ticket automatically. - The returned ticket ID is stored on the work order for all future sync events. - Status, notes, and attachments flow from Field Nation back to the ticket as work happens. - Inbound callbacks continue to work — ConnectWise tickets still trigger work orders via the callback flow. This mode works best when ConnectWise is the buyer-facing system of record and Field Nation drives technician dispatch and execution. --- ## Configuration stages ### Confirm environment and scope Before creating accounts or generating keys, decide which ConnectWise instance you'll use (sandbox to test, production to go live) and which service boards and ticket types should sync. You can decide these yourself — loop in your Field Nation Solutions Engineer only if unsure. ### Create a dedicated API member and security role In ConnectWise, create a new API member (non-human service account) and a security role named **Field Nation Integration** with the minimum required permissions. Do not use a personal admin account or reuse an existing role. ### Generate API keys From the API member record in ConnectWise, generate a Public/Private key pair. Copy the Private Key immediately — it is only shown once. Store both keys and the Company Name in a password manager. ### Connect and configure Field Nation Log in to Field Nation as a Company Admin and navigate to **Integrations → Field Services → ConnectWise → Manage**. Enter your API credentials and click **Save** — the connector validates authentication. Once authenticated, click **Refresh Fields** to pull your ConnectWise boards and fields, then enable sync and configure your work order field mappings. ### Configure callbacks in ConnectWise Register a callback in ConnectWise pointing to your Field Nation **Trigger URL** via the REST API — see the [Callback Setup guide](/docs/connectors/platforms/connectwise/guides/callback-setup). ### Validate end-to-end Create a test ticket in ConnectWise that meets your callback conditions. Confirm the work order is created in Field Nation. Update the work order and confirm the ConnectWise ticket reflects the change. --- ## Before you begin > [INFO] **Your ConnectWise environment is unique.** Several values in this guide differ by instance and must come from your own ConnectWise admin — don't copy the examples: - **Host URL** (e.g. `api-na.myconnectwise.net`) - **Service boards** in scope — boards organize tickets by team or workflow (e.g., "Field Services"); each board has its own set of statuses - **Status values** that trigger callbacks — enter the **exact status text name** as it appears in ConnectWise (e.g., `Dispatch to FN`), not a numeric ID. Status names are board-specific — use **Refresh Fields** to confirm the available values for your boards Confirm these before you start. Complete the checks below before starting the [Configuration](/docs/connectors/platforms/connectwise/configuration) steps. **Stage 1** (ConnectWise prerequisites) can be completed by a ConnectWise administrator independently — it does not require Field Nation access. **Stage 4** (callback setup) requires the Trigger URL from Field Nation settings — coordinate with the Field Nation Integration Owner before starting it. **Stages 2, 3, and 5** require Company Admin access in Field Nation. ### ConnectWise requirements #### API access and license The integration requires a ConnectWise Manage license with API access and the ability to create API-type members. | Requirement | Where to confirm | |---|---| | API member type available | System → Members → API Members tab → New Member — "API Member" | | Ability to create Security Roles | System → Security Roles | | Ability to configure Callbacks | Via REST API — `POST /v4_6_release/apis/3.0/system/callbacks` | #### Minimum security role permissions Create a security role named **Field Nation Integration** and assign it to your API member. The full permissions table — including module paths, per-feature breakdowns, and what to explicitly exclude — is in [Configuration Step 3](/docs/connectors/platforms/connectwise/configuration#step-3-create-a-security-role-and-assign-minimum-permissions). ### Field Nation requirements | Requirement | Details | |---|---| | Company Admin access | Required to access **Integrations** and save connector settings | | Target environment identified | Configure sandbox before production | | Template Ticket ID | An active ConnectWise ticket accessible by your API member, used to discover available mapping fields. Choose one with all intended field types populated (board, type, subtype, priority, custom fields) — a sparse ticket limits available mappings. | --- ## What syncs ### Inbound (ConnectWise → Field Nation) | Data from ConnectWise | Notes | |---|---| | Service ticket summary and description | Title, initial description, initial internal analysis | | Status | Mapped to Field Nation work order status via status mapping | | Contact | Associated contact on the ticket | | Site | Maps to Field Nation work location | | Custom fields | Any custom fields on the ticket, discovered via Template Ticket ID | | Ticket notes | Synced as messages when message sync is enabled (requires the **Messages** entitlement) | | Attachments | Synced when the **Client Documents → Import** toggle is enabled (requires the **Client Documents** contract feature). Imported at work-order creation only — files added to the ConnectWise ticket after the callback fires are not pulled retroactively. | | Schedule entries | Start/end dates, when `schedule.dateStart`/`schedule.dateEnd` are mapped | ### Outbound (Field Nation → ConnectWise) | Field Nation event | ConnectWise entity written | |---|---| | Work order status changes | Service ticket status updated | | Message posted on work order | Ticket Note created (internal or external, configurable) | | Work order created (Outbound Create enabled) | New Service Ticket | | File attached to work order | Document uploaded when the **Client Documents → Export** toggle is enabled (requires **Client Documents**); always created as private in ConnectWise (hardcoded, not configurable) | | Schedule updated | Schedule Entry updated (when `schedule.dateStart`/`schedule.dateEnd` are mapped) | | Sync transaction fails | Failure notification sent to configured email addresses — optional, requires **Notification Email Addresses** set in connector settings | --- ## Next steps Your ConnectWise administrator should work through the [Configuration](/docs/connectors/platforms/connectwise/configuration) guide to create the API member, security role, and API keys, then connect the Field Nation connector. Once configured, see [Workflow Setup](/docs/connectors/platforms/connectwise/workflow) to configure callbacks in ConnectWise and activate sync. For programmatic or multi-board callback registration, see the [Callback Setup guide](/docs/connectors/platforms/connectwise/guides/callback-setup). --- ### Workflow URL: /docs/connectors/platforms/connectwise/workflow The ConnectWise connector supports two independent sync directions. Each is configured separately. | Direction | How it works | Requires | |---|---|---| | **Inbound** (CW → FN) | ConnectWise fires a callback when ticket conditions are met; Field Nation fetches the ticket and creates or updates a work order | Callback configured in ConnectWise + connector configured in Field Nation | | **Outbound** (FN → CW) | Field Nation work order events push status, notes, and attachments to the ConnectWise ticket | Connector configured + outbound field mappings set | --- ## Inbound — configure ConnectWise callbacks A callback is the mechanism that notifies Field Nation when a ticket is ready for dispatch. When it fires, ConnectWise posts the ticket ID to your Field Nation trigger URL. The connector fetches the full ticket and creates or updates the work order. > ConnectWise system callbacks are managed through the REST API — there is no callback UI unless your instance already has one. Follow the **[Callback Setup guide](/docs/connectors/platforms/connectwise/guides/callback-setup)** to register via Postman or cURL. **Before you start:** Complete [Steps 1–5 of Configuration](/docs/connectors/platforms/connectwise/configuration) and have your Field Nation trigger URL ready. ### Get your trigger URL **In Field Nation:** Integrations → Field Services → ConnectWise → Manage → Settings → Trigger URL Copy the full URL including the security token — it authenticates all inbound requests. Treat the token as a secret. ``` https://micro.fieldnation.com/v1/broker/inbound?client_token= # production https://micro.fndev.net/v1/broker-sandbox/inbound?client_token= # sandbox ``` > The trigger URL is environment-specific and unique to your account — always copy the exact value from your environment's Settings → Trigger URL. ### Choose your trigger status Decide which ConnectWise ticket status should fire inbound sync — for example, `"Dispatch to FN"`. When a ticket **transitions into** that status, the callback fires and Field Nation receives the ticket ID. The callback is tied to a **numeric status ID**, not the status name. The [Callback Setup guide — Step 2](/docs/connectors/platforms/connectwise/guides/callback-setup#step-2-discover-the-target-status-id) shows how to look up that ID for each board. **Option A — Status-based (most common and simplest)** Register a callback with `level: status` and the numeric ID of your trigger status (e.g., `"Dispatch to FN"`). Only tickets transitioning into that status fire. See the [Callback Setup guide](/docs/connectors/platforms/connectwise/guides/callback-setup). **Option B — Board-based or custom field (requires a ConnectWise Workflow Rule)** The CW callback API only supports status-level triggers. For board-based or custom-field conditions, configure a **ConnectWise Workflow Rule** that transitions the ticket into your trigger status when the condition is met — then the status-based callback fires normally. Example Workflow Rule conditions: - Board = "Field Services" → set status to "Dispatch to FN" - Custom Field "Send to FN" = true → set status to "Dispatch to FN" > [INFO] If your callback fires on every ticket update (re-transitions included), add a Workflow Rule condition that excludes tickets where a custom field (e.g. `FN Sync Status`) is already set to "Sent". Set that field via an outbound Field Nation mapping after the first successful sync. ### Register the callback Follow the **[Callback Setup guide](/docs/connectors/platforms/connectwise/guides/callback-setup)** to register via Postman or cURL. The guide covers: - **Step 2** — Discovering the numeric status ID for your chosen trigger status (IDs are board-specific) - **Step 3** — Checking for an existing callback before registering to avoid duplicates - **Step 4** — The POST request with your trigger URL and status ID Append `&external_id=` to your trigger URL in the registration payload so ConnectWise appends the ticket ID on delivery. ### Test the callback 1. Update a test ticket in ConnectWise to meet your trigger conditions 2. In ConnectWise: **System → Setup Tables → Integrator Login** → open your integration → confirm HTTP 200 in the callback log 3. Check Field Nation — confirm a work order was created from the ticket --- ## Outbound workflows (Field Nation → ConnectWise) > **Reference and list fields fail if the value doesn't match exactly.** Free-text fields (summary, description, address) are safe; anything else (country, status, type, board, dropdown custom fields) must be set to **inbound-only (←)** or map to an exact ConnectWise value. See [Configuration → Step 6](/docs/connectors/platforms/connectwise/configuration#step-6-configure-field-and-status-mappings). ### Status sync When a Field Nation work order status changes, the connector updates the linked ConnectWise ticket status using your status mapping configuration. Configure under **Field Nation → Integrations → Field Services → ConnectWise → Field Mappings → Status**. Map ConnectWise and Field Nation statuses to each other in the Status mapping table. Values are populated after Refresh Fields runs. ### Notes sync When a message is posted on a Field Nation work order, the connector creates a **Ticket Note** in ConnectWise. | Setting | Behavior | |---|---| | Message sync disabled | Notes do not sync | | Message sync enabled, public | Note written to the ConnectWise **Detail Description** field — visible to contacts | | Message sync enabled, internal | Note written to the ConnectWise **Internal Analysis** field — internal only | ### Attachment sync When a **provider** uploads a file to a Field Nation work order, the connector uploads it to ConnectWise Documents under the linked service ticket. (Buyer-side Client Documents uploads do not trigger outbound — only provider uploads.) > [INFO] Attachment sync is controlled in the **Client Documents** mapping section — *"Import all files from Connectwise ticket to Client Documents…"* (inbound) and *"Send provider uploads to Connectwise as Document"* (outbound) — and requires the **Client Documents** contract feature. Outbound documents are always created as **private** in ConnectWise (hardcoded, not configurable). The **Deliverables → Access Control** setting only controls inbound visibility — files synced from ConnectWise into Field Nation. If the toggles don't appear, contact [Field Nation Support](https://app.fieldnation.com/support-cases) to enable the Client Documents entitlement. The connector checks existing documents on the ticket before each upload using a fingerprint derived from the file's upload ID. Files already present are skipped — no duplicate uploads occur. Requires **Companies → Manage Attachments — Add** on the API member's security role (the same permission's **Inquire** level is also what lets the connection read ticket documents at all). ### Outbound Create When enabled, every new Field Nation work order automatically creates a new ConnectWise service ticket. The returned ticket ID is stored on the work order for all future sync events. Enable in **Field Nation → Integrations → Field Services → ConnectWise → Manage → Settings → Outbound Create** (toggle). Requires **Service Tickets — Add** permission on the security role. --- ## Workflow diagrams ### Inbound (ConnectWise → Field Nation) ```mermaid --- config: flowchart: curve: linear --- flowchart LR T([Ticket updated in ConnectWise]) --> C{Callback conditions met?} C -->|No| SKIP([No action]) C -->|Yes| CB[Callback fires → POST ticket ID] CB --> F[Connector fetches ticket + related data] F --> M[Apply inbound field mappings] M --> WO([Work order created or updated in FN]) ``` ### Outbound (Field Nation → ConnectWise) ```mermaid --- config: flowchart: curve: linear --- flowchart LR E([Work order event]) --> B{Event type} B -->|Status change| S[Ticket status updated] B -->|Message posted| N[Ticket Note created] B -->|File attached| D[Document uploaded] B -->|Outbound Create| T[New Service Ticket created] B -->|Schedule updated| SC[Schedule Entry updated] ``` --- ## Troubleshooting **Check:** - Confirm the **Trigger On Status** value in Field Nation connector settings matches the ticket's current status text exactly — casing and spacing must be identical. This is the most common cause: the callback fires but Field Nation silently skips the ticket because the status text doesn't match. - Confirm the callback URL in ConnectWise matches your Field Nation trigger URL exactly — including the full `client_token` - In ConnectWise: **System → Setup Tables → Integrator Login** → open your integration → check the callback log for the HTTP response code - A `200` response means Field Nation received the request — check Field Nation integration logs for processing errors - A `4xx` or `5xx` response means delivery failed — confirm the URL is correct and accessible - Confirm the integration is enabled in Field Nation connector settings **Check:** - Confirm the callback is registered and active — `inactiveFlag` must be `false` (`true` means disabled); list callbacks via the REST API (see the [Callback Setup guide](/docs/connectors/platforms/connectwise/guides/callback-setup)) - Confirm the callback is watching the correct numeric status ID (`objectId`) for the board - Confirm the ticket actually meets the conditions — verify the status, board, or custom field value matches exactly **Cause:** The callback fires on every ticket update, not just the first dispatch event. **Resolve:** - Add a custom field (e.g. `FN Sync Status`) and set it to a "sent" value via an outbound mapping after the first successful import - Add a condition to the callback or Workflow Rule that excludes tickets where `FN Sync Status = Sent` **Check:** - Confirm outbound field mappings include a status mapping for the relevant Field Nation statuses - Confirm the API member has **Service Tickets — Edit** permission - Confirm ConnectWise status values in the mapping match exactly what Refresh Fields discovered - Check the notification email address configured in Field Nation for sync failure alerts **Check:** - Confirm **Message sync** is enabled in Field Nation connector settings - Confirm the API member has **Service Desk → Service Tickets — Inquire, Add, Edit** on the security role — Ticket Note sync rides on this permission (there is no separate Ticket Notes permission) - Confirm the work order has an active link to a ConnectWise ticket (the ticket ID must be stored on the work order) **Check:** - Confirm the **Client Documents** contract feature is enabled for your account — request it via [Field Nation Support](https://app.fieldnation.com/support-cases) if the Client Documents toggles don't appear - Confirm the API member has **Companies → Manage Attachments — Add** on the security role (a `403` on the upload step in Event History points here) - Confirm the upload came from the **provider** side of the work order — buyer-side Client Documents uploads don't trigger outbound - Check if the file is a duplicate — the connector skips files already uploaded using fingerprint matching **Expected behavior:** Schedule sync requires **Service Desk (or Time & Expense) → Resource Scheduling — Inquire, Add, Edit** on the security role. New schedule entries are also created with default assumptions — a fixed schedule type and a default member assignment — so if those defaults don't match your board, entries land on the wrong type or resource. **Failure signature:** dates don't appear on the ConnectWise schedule entry, or the entry lands on the wrong resource. This means the board's schedule type doesn't match the mapping. **Resolve (self-check):** - Confirm the mapping: `schedule.dateStart` → time window start, `schedule.dateEnd` → end - Remove the `schedule.dateStart` / `schedule.dateEnd` mappings, then run one test ticket in a sandbox environment - If the dates still don't land, correct the mapping to match your board's schedule type and retry — repeat until they appear before relying on it in production **Check:** - Confirm **Outbound Create** is enabled in Field Nation connector settings - Confirm the API member has **Service Tickets — Add** permission - Confirm outbound field mappings include `company`, `board`, and `summary` (all set to outbound →) — ConnectWise requires all three to create a ticket - Review sync failure notification emails for the specific API error --- ## Next steps Register callbacks via the ConnectWise REST API (Postman or cURL) — including multi-board setup and status-ID discovery. Create the API member, security role, and keys, then connect and map fields in Field Nation. --- ### Configuration URL: /docs/connectors/platforms/freshdesk/configuration ## Prerequisites - ☐ Freshdesk API key - ☐ Freshdesk subdomain (e.g., `company.freshdesk.com`) - ☐ Admin access to Field Nation --- ## Step 1: Get Freshdesk API Key ### Log into Freshdesk Use admin account ### Navigate to Profile Click profile icon → Profile Settings ### Copy API Key Right sidebar → "Your API Key" Click to reveal and copy --- ## Step 2: Configure in Field Nation Access Integration Broker and select "Freshdesk" ### Freshdesk Subdomain **Format:** `company` (from `company.freshdesk.com`) Example: If URL is `acme.freshdesk.com`, enter `acme` --- ### API Key Paste API key from Step 1 **Authentication Format:** ``` Username: {your_api_key} Password: X ``` Field Nation handles formatting automatically. --- ## Step 3: Test Connection Click "Test Connection" → Verify success --- ## Step 4: Refresh Fields & Map Click "Refresh Fields" → Configure mappings: **Inbound:** ``` subject → title description → description status → status_id (Array Map) priority → priority (Array Map) requester.name → contact.name due_by → schedule.start (Date Convert) ``` **Outbound:** ``` status.name → status (Array Map) completion_notes → private_note ``` **Status Mapping:** ``` Freshdesk: Open, Pending, Resolved, Closed Field Nation: Draft, Assigned, Work Done, Approved ``` --- ## Step 5: Save & Get Trigger URL Save → Copy trigger URL for Freshdesk webhook --- ## Troubleshooting - Verify API key correct - Check subdomain spelling - Ensure API access enabled - Check API key permissions - Verify custom fields accessible - Try "Refresh Fields" again --- --- ### Overview URL: /docs/connectors/platforms/freshdesk/overview ## Overview The Freshdesk connector enables seamless integration between Freshdesk and Field Nation: - **Ticket-based dispatch**: Create work orders from Freshdesk tickets - **REST API integration**: Simple authentication via API key - **Webhook triggers**: Real-time notifications - **Bidirectional sync**: Status and notes flow both directions - **Custom fields support**: Map Freshdesk custom fields --- ## At a Glance API Key (Basic Auth) Tickets Webhooks (automation rules) Bidirectional (create, update, notes) --- ## How It Works ### Configure Automation Set up automation rule in Freshdesk that triggers on ticket status/tag changes ### Webhook Fires Freshdesk sends ticket ID to Field Nation when conditions met ### Fetch Ticket Data Integration Broker retrieves complete ticket from Freshdesk REST API ### Apply Mappings Transform Freshdesk ticket into Field Nation work order ### Create Work Order Work order created with correlation ID for bidirectional sync --- ## Common Use Cases ### Dispatch from Support Ticket ``` Ticket Status = "Field Service Required" → Webhook triggers → Work order created ``` ### Tag-Based Dispatch ``` Tag = "on-site" → Automation rule fires → Field technician dispatched ``` ### Status Sync Back ``` FN Work Order = "Completed" → Freshdesk Ticket Status = "Resolved" → Private note added ``` --- ## Features ### Field Mapping Capabilities - **Standard Fields**: Subject, Description, Status, Priority, Due By - **Custom Fields**: All custom ticket fields - **Requester Info**: Name, email, phone, company - **Attachments**: Optional file sync - **Tags**: Map Freshdesk tags ### Supported Operations --- ## Prerequisites ### Freshdesk Requirements Generate in Freshdesk: 1. Profile Settings → View Profile 2. Your API Key (right sidebar) 3. Copy key for Field Nation - Admin or account admin role - Ability to create automation rules - Ability to configure webhooks API key must have: - Read access to tickets - Write access for status updates - Access to custom fields --- ## Authentication Freshdesk uses **Basic Authentication** with API key: ``` Format: username:password Username: API Key Password: X (literal "X") ``` **Example:** ``` Authorization: Basic base64(your_api_key:X) ``` > [INFO] **Simple Setup**: Freshdesk uses API key as username, literal "X" as password for Basic Auth. --- ## Best Practices - ✅ Use automation rules for triggering (not manual webhooks) - ✅ Test in sandbox Freshdesk account first - ✅ Map priority/status correctly - ✅ Use tags for workflow control - ✅ Monitor webhook delivery --- --- ### Workflow URL: /docs/connectors/platforms/freshdesk/workflow ## Prerequisites - ☐ [Field Nation configuration complete](/docs/connectors/platforms/freshdesk/configuration) - ☐ Trigger URL copied - ☐ Freshdesk admin access --- ## Configure Automation in Freshdesk ### Navigate to Automations Admin → Workflows → Automations ### Create Ticket Update Rule Click "+ New Rule" → Select "Ticket Updates" ### Configure Trigger **Rule Name**: `Send to Field Nation` **Description**: Auto-dispatch tickets requiring field service **When an action performed by**: Agent or Requester **Involves any of these events:** - ☑ Status is changed - ☑ Tag is added **On tickets with these properties:** ``` Status is "Field Service Required" OR Tags contains "on-site" ``` ### Configure Action **Perform these actions:** - Trigger webhook **Webhook URL**: Paste Field Nation trigger URL **Method**: POST **Content**: JSON **Request Body:** ```json { "ticket_id": "{{ticket.id}}", "status": "{{ticket.status}}", "priority": "{{ticket.priority}}" } ``` **Encoding**: JSON ### Save & Activate Save rule → Toggle to "Active" --- ## Test Integration ### Create Test Ticket 1. Create new ticket in Freshdesk 2. Set status to "Field Service Required" 3. Or add "on-site" tag 4. Save ### Verify Automation Check Admin → Audit Logs: - Look for automation execution - Verify webhook sent ### Check Field Nation Verify work order created with correct data --- ## Troubleshooting - Check rule is Active - Verify ticket meets conditions - Review Audit Logs for execution - Test with simple conditions first - Verify trigger URL correct - Check Freshdesk can reach URL - Review webhook response in logs - Test URL with curl/Postman - Check Field Nation config valid - Verify all required fields mapped - Review Integration Broker logs - Test with minimal ticket data --- ## Advanced Configuration ### Multiple Automation Rules **Rule 1**: Urgent Tickets ``` Priority = Urgent → Immediate dispatch ``` **Rule 2**: Standard Tickets ``` Status = "Field Service Required" Priority != Urgent → Normal dispatch ``` ### Prevent Duplicate Sends Add custom field: ``` Field: "Synced to FN" Type: Checkbox ``` Update rule conditions: ``` Status = "Field Service Required" AND "Synced to FN" is NOT true ``` After sync, set checkbox to true. --- --- ### Configuration URL: /docs/connectors/platforms/netsuite/configuration ## Prerequisites - ☐ NetSuite Account ID - ☐ Consumer Key & Secret (from Integration Record) - ☐ Token ID & Secret (from Access Token) - ☐ NetSuite instance URL - ☐ Admin access to Field Nation --- ## Step 1: Enable Token-Based Authentication ### Navigate to Features Setup → Company → Enable Features ### Enable TBA **SuiteCloud** tab → Check "Token-Based Authentication" ✅ ### Save Click "Save" to enable feature --- ## Step 2: Create Integration Record ### Navigate to Integrations Setup → Integration → Manage Integrations → New ### Configure Integration **Name**: `Field Nation Integration` **State**: Enabled ✅ **Concurrency Limit**: Leave default ### Save & Copy Credentials After save, copy: - **Consumer Key** (shown once) - **Consumer Secret** (shown once) Store securely immediately! --- ## Step 3: Generate Access Token ### Navigate to Access Tokens Setup → Users/Roles → Access Tokens → New ### Configure Token **Application Name**: Select "Field Nation Integration" (from Step 2) **User**: Select integration user (or create dedicated user) **Role**: Administrator or custom integration role **Token Name**: `Field Nation API Token` ### Save & Copy Credentials After save, copy: - **Token ID** - **Token Secret** (shown once!) > **Critical**: Token Secret displayed ONCE. Save immediately to secure password manager. --- ## Step 4: Get Account ID **Setup → Company → Company Information** **Account ID** field → Copy value Example: `1234567` or `1234567_SB1` (sandbox) --- ## Step 5: Configure in Field Nation Access Integration Broker → Select "NetSuite" ### Enter Credentials **Account ID**: From Step 4 **Consumer Key**: From Integration Record (Step 2) **Consumer Secret**: From Integration Record (Step 2) **Token ID**: From Access Token (Step 3) **Token Secret**: From Access Token (Step 3) --- ### Instance URL **Format:** ``` https://{account_id}.suitetalk.api.netsuite.com/services/NetSuitePort_2021_2 ``` **Sandbox:** ``` https://{account_id}_SB1.suitetalk.api.netsuite.com/services/NetSuitePort_2021_2 ``` Field Nation may auto-construct this from Account ID. --- ### Record Type **Supported:** - `serviceOrder` - Service orders (most common) - `supportCase` - Support cases - `customRecord_*` - Custom records --- ## Step 6: Test Connection Click "Test Connection" **Success** ✅: Credentials valid, SuiteTalk API accessible **Failure** ❌: Review error: - Verify all 5 credentials correct - Ensure Token not expired/revoked - Check Account ID includes sandbox suffix if applicable - Grant integration role full access to target records - Check user has role assigned - Verify TBA enabled --- ## Step 7: Refresh Fields & Map Click "Refresh Fields" → Configure mappings: **Inbound:** ``` title → title message → description status → status_id (Array Map) subsidiary.name → location.company.name ``` **Outbound:** ``` status.name → status (Array Map) approved_amount → actualCost ``` --- ## Step 8: Save Configuration Review settings → Save → Copy trigger URL --- ## Troubleshooting - Verify TBA enabled in features - Check all 5 credentials correct - Ensure token not revoked - Test with NetSuite REST API explorer - Verify record type exists - Check API name (camelCase) - For custom records: Use `customRecord_{id}` --- --- ### Overview URL: /docs/connectors/platforms/netsuite/overview ## Overview The NetSuite connector enables seamless integration between NetSuite ERP and Field Nation: - **Service order dispatch**: Create work orders from NetSuite service records - **SuiteTalk SOAP API**: Token-based authentication (TBA) - **SuiteScript integration**: Custom triggers via SuiteScripts - **Bidirectional sync**: Status, costs, and completion data flow both ways - **Custom fields support**: Map NetSuite custom fields --- ## At a Glance Token-Based Authentication (TBA) - Account ID + Consumer Key/Secret + Token Key/Secret Service Orders, Cases, Custom Records SuiteScript + Webhooks Bidirectional (create, update, financial) --- ## How It Works ### Deploy SuiteScript Deploy custom SuiteScript in NetSuite that monitors service records ### Script Triggers SuiteScript evaluates conditions (status change, custom field) and sends webhook ### Field Nation Receives Integration Broker receives notification with NetSuite record ID ### Fetch Record Data Broker retrieves complete record via SuiteTalk SOAP API ### Create Work Order Transformed data creates Field Nation work order --- ## Common Use Cases ### Service Order Dispatch ``` Service Order Status = "Ready for Dispatch" → SuiteScript triggers → Work order created → Service order updated ``` ### Case-Based Dispatch ``` Case Type = "On-Site Service" AND Status = "Escalated" → Webhook fires → Field technician dispatched ``` ### Financial Tracking ``` FN Work Order = "Approved" → NetSuite Service Order updated → Costs synchronized → Invoice generated ``` --- ## Features ### Field Mapping Capabilities - **Standard Fields**: Title, Message, Status, Sales Order Number - **Custom Fields**: All custom fields on target record type - **Related Records**: Customer, Contact, Location, Item - **Financial Fields**: Cost, revenue, billing codes - **Sublists**: Line items, time entries, expenses ### Supported Operations --- ## Prerequisites ### NetSuite Requirements Enable TBA in NetSuite: 1. Setup → Company → Enable Features 2. SuiteCloud → Token-Based Authentication ✅ 3. Save Create integration record: 1. Setup → Integration → Manage Integrations → New 2. Name: "Field Nation Integration" 3. State: Enabled 4. Copy: Consumer Key, Consumer Secret Generate access token: 1. Setup → Users/Roles → Access Tokens → New 2. Application Name: Field Nation Integration (from above) 3. User: Integration user 4. Role: Administrator or custom integration role 5. Copy: Token ID, Token Secret - Ability to deploy SuiteScripts - Admin or developer role - Access to target record types --- ## Authentication NetSuite uses **Token-Based Authentication (TBA)** with OAuth 1.0: ``` Account ID: {account_id} Consumer Key: {consumer_key} Consumer Secret: {consumer_secret} Token ID: {token_id} Token Secret: {token_secret} ``` **Generate Credentials:** 1. Enable TBA in NetSuite Features 2. Create Integration Record → Get Consumer Key/Secret 3. Generate Access Token → Get Token ID/Secret 4. Note Account ID (found in Setup → Company Information) > **Security**: Token Secret shown ONCE during generation. Save immediately to secure location. --- ## Best Practices - ✅ Use dedicated integration role - ✅ Test SuiteScripts in sandbox account - ✅ Monitor SuiteTalk API usage - ✅ Handle NetSuite's complex data model carefully - ✅ Document SuiteScript logic thoroughly --- ## Limitations - SOAP-based API (more complex than REST) - Token rotation required periodically - SuiteScript deployment requires technical expertise - Sublist/line item mapping can be complex --- --- ### Workflow URL: /docs/connectors/platforms/netsuite/workflow ## Prerequisites - ☐ [Field Nation configuration complete](/docs/connectors/platforms/netsuite/configuration) - ☐ Trigger URL copied - ☐ NetSuite admin/developer access - ☐ SuiteScript knowledge (or developer available) --- ## NetSuite Integration Approach NetSuite integration requires **SuiteScript deployment** to trigger webhooks: ```mermaid graph LR A[Record Created/Updated] --> B[User Event Script] B --> C{Conditions Met?} C -->|Yes| D[Send HTTPS Request] D --> E[Field Nation] C -->|No| F[No Action] ``` --- ## Step 1: Create SuiteScript Deploy User Event Script to trigger on record changes: ### Sample SuiteScript 2.0 ```javascript /** * @NApiVersion 2.1 * @NScriptType UserEventScript */ define(['N/https', 'N/record'], (https, record) => { const afterSubmit = (context) => { try { const newRecord = context.newRecord; const status = newRecord.getValue({ fieldId: 'status' }); // Only trigger if status = "Ready for Dispatch" if (status === 'Ready for Dispatch') { const recordId = newRecord.id; const recordType = newRecord.type; // Field Nation trigger URL const fnUrl = 'https://api.fieldnation.com/integrations/trigger/{YOUR_CLIENT_TOKEN}'; const payload = { recordId: recordId, recordType: recordType, timestamp: new Date().toISOString() }; const response = https.post({ url: fnUrl, body: JSON.stringify(payload), headers: { 'Content-Type': 'application/json' } }); log.audit({ title: 'Field Nation Trigger Sent', details: 'Record ID: ' + recordId + ', Response: ' + response.code }); // Optional: Update custom field to track sync // record.submitFields({ // type: recordType, // id: recordId, // values: { custbody_fn_synced: true } // }); } } catch (e) { log.error({ title: 'Field Nation Trigger Error', details: e.message }); } }; return { afterSubmit: afterSubmit }; }); ``` --- ## Step 2: Upload & Deploy Script ### Create Script File Save the script as `fieldnation_trigger_ue.js` ### Upload to File Cabinet Customization → Scripting → Scripts → Files → SuiteScripts → Upload ### Create Script Record Customization → Scripting → Scripts → New **Name**: Field Nation Trigger **ID**: `customscript_fn_trigger` **Script File**: Select uploaded file **Function**: `afterSubmit` ### Create Script Deployment **Applied To**: Select record type (Service Order, Case, etc.) **Status**: Testing (initially) **Execute As Role**: Administrator **Log Level**: Debug (for testing) **Audience**: All Roles ### Save Deployment Click "Save" --- ## Step 3: Test Integration ### Create Test Record 1. Create Service Order in NetSuite 2. Set status to "Ready for Dispatch" 3. Save record ### Verify Script Execution Customization → Scripting → Script Execution Log Look for "Field Nation Trigger Sent" log entry ### Check Field Nation Verify work order created with correct data ### Review Logs Check for any errors in execution log --- ## Advanced Configuration ### Conditional Triggering Add complex logic to SuiteScript: ```javascript // Only trigger for specific subsidiaries const subsidiary = newRecord.getValue({ fieldId: 'subsidiary' }); if (subsidiary !== 'US Operations') return; // Only for high-priority orders const priority = newRecord.getValue({ fieldId: 'priority' }); if (priority < 3) return; // Check custom field const sendToFN = newRecord.getValue({ fieldId: 'custbody_send_to_fn' }); if (!sendToFN) return; ``` --- ### Prevent Duplicate Sends ```javascript // Check if already synced const fnSynced = newRecord.getValue({ fieldId: 'custbody_fn_synced' }); if (fnSynced) { log.audit('Already synced', 'Skipping'); return; } // Send webhook... // Mark as synced record.submitFields({ type: newRecord.type, id: newRecord.id, values: { custbody_fn_synced: true }, options: { enableSourcing: false, ignoreMandatoryFields: true } }); ``` --- ## Troubleshooting - Check deployment status = "Released" - Verify record type matches deployment - Check audience includes current user - Review script execution log for errors - Verify trigger URL correct - Check NetSuite outbound HTTPS enabled - Review Governance limits (10 HTTPS requests per script) - Test URL externally (curl/Postman) - Check Field Nation credentials valid - Verify SuiteTalk API accessible - Review Integration Broker logs - Test with minimal field set --- ## Production Deployment ### Test in Sandbox Deploy and test in NetSuite sandbox account ### Update Script Status Change deployment status to "Released" ### Monitor Execution Watch script execution logs for first week ### Optimize Adjust conditions, improve performance, reduce API calls --- --- ### Configuration URL: /docs/connectors/platforms/quickbase/configuration ## Prerequisites - ☐ Quickbase User Token - ☐ Quickbase Realm (subdomain) - ☐ App ID and Table ID - ☐ Admin access to Field Nation --- ## Step 1: Generate Quickbase User Token ### Log into Quickbase Use admin or dedicated integration user account ### Navigate to User Tokens Profile → My Preferences → Manage User Tokens ### Create New Token Click "+ New User Token" **Name**: `Field Nation Integration` **Description**: Integration between Quickbase and Field Nation **Applications**: Select apps that need access **Token**: Copy immediately (shown once!) --- ## Step 2: Get App & Table Information ### Find App ID Open your Quickbase app → URL shows App ID: ``` https://realm.quickbase.com/db/{app_id} ``` Example: `br5k3a4mn` ### Find Table ID Open table → URL shows Table ID: ``` https://realm.quickbase.com/db/{table_id} ``` Example: `bskt8r3fn` ### Note Realm Your Quickbase subdomain: ``` https://{realm}.quickbase.com ``` Example: If URL is `acme.quickbase.com`, realm is `acme` --- ## Step 3: Configure in Field Nation Access Integration Broker → Select "Quickbase" ### Realm Enter your Quickbase realm (subdomain) Example: `acme` (from `acme.quickbase.com`) --- ### User Token Paste user token from Step 1 **Authentication Header:** ``` QB-USER-TOKEN {your_user_token} ``` --- ### App ID Enter App ID from Step 2 Example: `br5k3a4mn` --- ### Table ID Enter Table ID from Step 2 Example: `bskt8r3fn` --- ## Step 4: Test Connection Click "Test Connection" **Success** ✅: Token valid, app & table accessible **Failure** ❌: Check: - Verify token copied correctly - Ensure token not revoked/expired - Check token has access to app - Verify App ID correct - Check Table ID correct - Ensure token user has table access - Grant token access to app - Check user has table read/write permissions - Verify field-level permissions --- ## Step 5: Refresh Fields & Map Click "Refresh Fields" → Quickbase API queries table schema **Inbound Mappings:** ``` Field 6 (Title) → title Field 7 (Description) → description Field 11 (Status) → status_id (Array Map) Field 12 (Customer Name) → location.company.name Field 14 (Due Date) → schedule.start (Date Convert) ``` **Field IDs**: Quickbase uses numeric field IDs (Field 6, Field 7, etc.) **Outbound Mappings:** ``` status.name → Field 11 (Array Map) completion_notes → Field 20 completion_date → Field 21 (Date Convert) ``` [Field mapping guide →](/docs/connectors/concepts/field-mappings) --- ## Step 6: Save Configuration Review → Save → Copy trigger URL for Quickbase automation --- ## Troubleshooting - Verify token has table access - Check Table ID correct - Try "Refresh Fields" again - Review API response in logs - Check field IDs correct - Verify data type compatibility - Use proper transformation actions - Test with sample data --- --- ### Overview URL: /docs/connectors/platforms/quickbase/overview ## Overview The Quickbase connector enables seamless integration between Quickbase applications and Field Nation: - **Table-based sync**: Create work orders from any Quickbase table - **REST API integration**: Modern JSON API with user tokens - **Webhook triggers**: Real-time notifications via Quickbase automations - **Bidirectional sync**: Data flows both directions automatically - **Custom fields support**: Map any Quickbase field --- ## At a Glance User Token (Bearer token) Any Quickbase table in your apps Quickbase Automations (webhooks) Bidirectional (create, update) --- ## How It Works ### Configure Automation Create Quickbase automation that triggers on record changes ### Webhook Fires Quickbase sends record ID to Field Nation when conditions met ### Fetch Record Data Integration Broker retrieves complete record from Quickbase REST API ### Apply Mappings Transform Quickbase record into Field Nation work order ### Create Work Order Work order created with correlation ID for bidirectional sync --- ## Common Use Cases ### Service Request Dispatch ``` Table: Service Requests Trigger: Status = "Approved" → Work order created in Field Nation ``` ### Work Order Tracking ``` Table: Field Work Orders Trigger: New record added → Automatic Field Nation dispatch ``` ### Status Synchronization ``` FN Work Order = "Completed" → Quickbase record Status = "Complete" → Completion date updated ``` --- ## Features ### Field Mapping Capabilities - **All Field Types**: Text, numeric, date, checkbox, user, relationship - **Formula Fields**: Access calculated values - **Related Tables**: Map fields from related tables via relationships - **Multi-select fields**: Handle comma-separated values - **File Attachments**: Optional file sync ### Supported Operations --- ## Prerequisites ### Quickbase Requirements Generate in Quickbase: 1. Profile → Manage User Tokens 2. "+ New User Token" 3. Name: "Field Nation Integration" 4. Assign all apps needed 5. Copy token (shown once) - Admin access to Quickbase app - Table must have webhook automation capabilities - User token must have access to table - Read access to all fields to sync - Write access for status updates - Access to related tables --- ## Authentication Quickbase uses **User Token** (Bearer token): ``` Authorization: QB-USER-TOKEN {user_token} ``` **Generate Token:** 1. Quickbase → Profile → Manage User Tokens 2. New User Token 3. Select applications to grant access 4. Copy token immediately (shown once) > **Security**: User tokens have same permissions as user. Create dedicated integration user with minimum required access. --- ## Best Practices - ✅ Use dedicated integration user - ✅ Grant minimum table access needed - ✅ Test in sandbox app first - ✅ Use Quickbase automations (not external webhooks) - ✅ Monitor API usage limits --- ## Limitations - API rate limits: 10 requests/second per user token - Each sync consumes 2-3 API calls - Complex relationships may require custom JSONNET - Formula fields are read-only --- --- ### Workflow URL: /docs/connectors/platforms/quickbase/workflow ## Prerequisites - ☐ [Field Nation configuration complete](/docs/connectors/platforms/quickbase/configuration) - ☐ Trigger URL copied - ☐ Quickbase app admin access --- ## Configure Automation in Quickbase ### Navigate to Automations Open your Quickbase app → Settings (gear icon) → Automations ### Create New Automation Click "+ Create a new automation" ### Configure Trigger **Name**: `Send to Field Nation` **Trigger**: "When a record is created or updated" **Table**: Select your table (e.g., Service Requests) **Conditions**: ``` Status is "Approved" OR Send to FN checkbox is checked ``` ### Add Webhook Action **Action Type**: "Make a request to a webhook" **Method**: POST **URL**: Paste Field Nation trigger URL **Headers**: ``` Content-Type: application/json ``` **Body**: ```json { "record_id": "[Record ID#]", "table_id": "[Table ID]", "app_id": "[App ID]" } ``` Use Quickbase merge fields (buttons in automation builder) to insert dynamic values. ### Save & Activate Save automation → Toggle to "Active" --- ## Test Integration ### Create Test Record 1. Open your Quickbase table 2. Add new record 3. Set status to "Approved" (or check trigger field) 4. Save ### Verify Automation Quickbase → Settings → Automations → View your automation Check "Recent runs" for execution ### Check Field Nation Verify work order created with correct data --- ## Advanced Configuration ### Conditional Logic Add complex conditions: ``` Status = "Approved" AND Priority ≥ High AND NOT(Customer Name is blank) AND Scheduled Date is not in the past ``` --- ### Prevent Duplicate Sends Add checkbox field: "Synced to FN" **Automation Conditions:** ``` Status = "Approved" AND "Synced to FN" is NOT checked ``` **Add Second Action:** Update record → Set "Synced to FN" = checked --- ### Multiple Automations **Automation 1**: Urgent Requests ``` Priority = "Urgent" → Immediate dispatch ``` **Automation 2**: Standard Requests ``` Status = "Approved" AND Priority != "Urgent" → Normal dispatch ``` --- ## Troubleshooting - Check automation is "Active" - Verify record meets conditions - Review "Recent runs" for errors - Test with simple condition first - Verify trigger URL correct - Check webhook response in automation log - Test URL with external tool - Review Field Nation logs - Verify Field Nation credentials valid - Check all required fields mapped - Review Integration Broker logs - Test with minimal record data --- ## Monitoring ### Automation Logs Quickbase → Automations → Your automation → "Recent runs" Review: - Execution timestamps - Conditions met/not met - Webhook responses - Error messages --- --- ### Configuration URL: /docs/connectors/platforms/rest-connector/configuration ## Prerequisites Before starting, gather: - ☐ OpenAPI specification file (JSON or YAML) - ☐ API credentials (username and password for Basic Auth) - ☐ API base URL - ☐ Endpoint paths for: - Record metadata (schema/fields) - Record fetch (get by ID) - Record create - Record update - Attachments upload (optional) - ☐ Admin access to Field Nation integrations --- ## Step 1: Access REST Connector Navigate to Field Nation's Integration Broker: **Sandbox**: [ui-sandbox.fndev.net/integrations](https://ui-sandbox.fndev.net/integrations) **Production**: [app.fieldnation.com/integrations](https://app.fieldnation.com/integrations) ### Log In Use your Field Nation buyer account credentials ### Select REST Connector Find "REST Connector" in the list of available connectors ### Click Configure Open the REST Connector configuration form --- ## Step 2: Upload OpenAPI Specification The OpenAPI spec enables automatic endpoint discovery and field mapping. ### Prepare Your Spec File **Supported Formats:** - OpenAPI 3.x (JSON or YAML) - OpenAPI 2.x / Swagger 2.0 (JSON or YAML) **Where to Get It:** - API documentation portal - `/swagger.json` or `/api-docs` endpoint - API provider's developer resources - Tools: Postman, Swagger UI, Stoplight **Validate Before Upload:** ```bash # Using Swagger Editor (online or local) https://editor.swagger.io/ # Or CLI tools swagger-cli validate openapi.json ``` --- ### Upload Process ### Click "Upload OpenAPI Spec" In the REST Connector configuration form ### Select File Choose your OpenAPI spec file (`.json` or `.yaml`/`.yml`) ### Validate Integration Broker parses and validates the specification ### Confirm Success Green confirmation message: "OpenAPI specification uploaded successfully" **What Happens:** - Broker parses spec to discover endpoints - Extracts request/response schemas - Populates endpoint dropdown menus - Enables field discovery --- ## Step 3: Configure Endpoints Select which API endpoints to use for each operation. ### Record Metadata Endpoint **Purpose**: Get field definitions and schema information **Required**: Yes - enables field mapping UI **Example Endpoints:** - `GET /api/v1/tickets/metadata` - `GET /api/v1/schema/ticket` - `OPTIONS /api/v1/tickets` (if returns schema) **Configuration:** 1. Click "Record Metadata" dropdown 2. Select endpoint from list (populated from OpenAPI spec) 3. Select HTTP method (usually `GET`) **Dropdown Contents:** - All endpoints from uploaded OpenAPI spec - Filtered to relevant operations - Shows path + method (e.g., `GET /api/v1/metadata`) --- ### Record Fetch Endpoint **Purpose**: Retrieve single record by ID **Required**: Yes - fetches data for inbound sync **Example Endpoints:** - `GET /api/v1/tickets/{id}` - `GET /api/v1/records/{ticket_id}` - `GET /api/v1/workorders/{wo_id}` **Configuration:** 1. Click "Record Fetch" dropdown 2. Select endpoint with ID parameter 3. Method: `GET` **ID Parameter:** - Endpoint must accept ID parameter (path or query) - Example: `/tickets/{id}` or `/tickets?id={id}` - Broker substitutes actual ID at runtime --- ### Record Create Endpoint **Purpose**: Create new records in external system **Required**: Yes - core functionality **Example Endpoints:** - `POST /api/v1/tickets` - `POST /api/v1/workorders/create` - `PUT /api/v1/records` (if PUT creates) **Configuration:** 1. Click "Record Create" dropdown 2. Select creation endpoint 3. Method: usually `POST` (sometimes `PUT`) **Request Body:** - Broker constructs body from field mappings - Format determined by OpenAPI spec (JSON, XML, etc.) - Uses schema defined in spec for validation --- ### Record Update Endpoint **Purpose**: Update existing records **Required**: Recommended (enables bidirectional sync) **Example Endpoints:** - `PUT /api/v1/tickets/{id}` - `PATCH /api/v1/tickets/{id}` - `POST /api/v1/tickets/{id}/update` **Configuration:** 1. Click "Record Update" dropdown 2. Select update endpoint 3. Method: `PUT`, `PATCH`, or `POST` **PUT vs PATCH:** - `PUT`: Full record replacement - `PATCH`: Partial update (only changed fields) - Check your API's behavior - configure accordingly --- ### Attachments Upload Endpoint **Purpose**: Upload files/documents to records **Required**: Optional - only if file sync needed **Example Endpoints:** - `POST /api/v1/tickets/{id}/attachments` - `POST /api/v1/files/upload` - `PUT /api/v1/records/{id}/documents` **Configuration:** 1. Click "Attachments Upload" dropdown 2. Select file upload endpoint 3. Method: usually `POST` **File Handling:** - Multipart/form-data or base64 encoding - Determined by OpenAPI spec - Broker handles encoding automatically --- ## Step 4: Set Authentication REST Connector supports **Basic Authentication** only. ### Enter Credentials **Format**: `username:password` **Examples:** ``` api_user:secure_password_123 integration@company.com:MyP@ssw0rd! client_12345:abc123xyz789 ``` **Configuration:** 1. Locate "Authentication" section 2. Enter credentials in format: `username:password` 3. Broker encodes automatically to `Authorization: Basic base64(username:password)` --- ### Security Best Practices Don't use personal account credentials. **Benefits:** - Better audit trails - Easier credential rotation - Integration doesn't break if personal account deactivated - Granular permission control Grant only required API access: - Read: Metadata, record fetch - Write: Record create, update - Upload: Attachments (if needed) **Don't grant:** - Admin privileges - Delete permissions (unless required) - Access to unrelated resources **Schedule:** - Every 90 days (recommended) - Immediately if compromise suspected - When team member with access leaves **Process:** 1. Generate new credentials in external system 2. Update Field Nation configuration 3. Test connection 4. Deactivate old credentials Field Nation handles storage securely: - Encrypted at rest (AES-256) - Never logged or exposed in errors - Transmitted over TLS 1.2+ **Your responsibility:** - Don't share credentials - Use strong, unique passwords - Document access in secure location --- ## Step 5: Test Connection Verify REST Connector can communicate with your API. ### Click "Test Connection" Button near authentication section ### Broker Validates - Sends request to Record Metadata endpoint - Authenticates using provided credentials - Parses response ### Review Result **Success**: ✅ Green confirmation - Connection successful - Endpoints reachable - Authentication valid **Failure**: ❌ Error message - Authentication failed: Check credentials - Endpoint not found: Verify URLs - Network error: Check firewall/VPN - SSL error: Verify certificate validity --- ### Common Connection Errors **Cause**: Invalid credentials **Solution:** 1. Verify username:password format correct 2. Check credentials valid in external system 3. Ensure API user has API access enabled 4. Test credentials with `curl`: ```bash curl -u "username:password" \ https://your-api.com/api/v1/metadata ``` **Cause**: Endpoint doesn't exist or wrong URL **Solution:** 1. Verify endpoint paths in OpenAPI spec 2. Check API base URL correct 3. Ensure endpoint actually exists in external system 4. Test endpoint directly with API client (Postman) **Cause**: Cannot reach external API **Solution:** 1. Verify API is publicly accessible (or VPN configured) 2. Check firewall rules 3. Whitelist Field Nation IP addresses 4. Verify SSL certificate valid (not expired) [Field Nation IPs →](/docs/resources/environments) **Cause**: Invalid or self-signed SSL certificate **Solution:** 1. Use valid, CA-signed certificate 2. Update certificate if expired 3. Ensure certificate matches domain 4. For testing: Use non-production environment with valid cert --- ## Step 6: Refresh Fields Discover available fields for mapping. ### Click "Refresh Fields" Button in field mapping section ### Broker Queries API - Calls Record Metadata endpoint - Retrieves field definitions - Parses schema from response ### Fields Populate Available fields appear in dropdown menus: - Standard fields - Custom fields - Required vs optional - Data types **Field Discovery Includes:** - Field names and API keys - Data types (string, integer, boolean, etc.) - Required/optional status - Enum values for picklists - Nested object structures --- ## Step 7: Configure Field Mappings Map data between Field Nation and your external system. ### Inbound Mappings (External → Field Nation) Map external system fields to Field Nation work order fields: **Example:** ``` External Field → Field Nation Field ────────────────────────────────── ticket_title → title ticket_description → description priority_level → priority (with Array Map) customer_name → location.company.name scheduled_date → schedule.start (with Date Convert) ``` ### Outbound Mappings (Field Nation → External) Map Field Nation fields to external system updates: **Example:** ``` Field Nation Field → External Field ──────────────────────────────────── status.name → ticket_status (with Array Map) assignee.user.email → technician_email completion_notes → resolution_notes approved_amount → final_cost ``` [Complete field mapping guide →](/docs/connectors/concepts/field-mappings) --- ### Field Mapping Actions Use standard transformation actions: **Sync**: Direct copy ```jsonnet { "source": "external_field", "target": "fn_field", "action": "sync" } ``` **Array Map**: Status mapping ```jsonnet { "source": "status", "target": "fn_status", "action": "array_map", "mappings": [ { "compare": "open", "value": "1" }, { "compare": "in_progress", "value": "2" }, { "compare": "completed", "value": "5" } ] } ``` **Date Convert**: Date formatting ```jsonnet { "source": "scheduled_date", "target": "schedule.start", "action": "date_convert", "input_format": "YYYY-MM-DD", "output_format": "YYYY-MM-DD HH:mm:ss" } ``` [All transformation actions →](/docs/connectors/concepts/field-mappings) --- ## Step 8: Save & Test ### Review Configuration Verify all settings: - ☐ OpenAPI spec uploaded - ☐ All required endpoints selected - ☐ Authentication configured - ☐ Connection test passed - ☐ Fields refreshed - ☐ Inbound mappings configured - ☐ Outbound mappings configured (if needed) ### Save Configuration Click "Save" to persist settings ### Test with Sample Data Create a test work order in Field Nation: 1. Trigger should sync to external system 2. Check Integration Broker logs 3. Verify record created in external system 4. Confirm field values correct ### Monitor Initial Syncs Watch first few synchronizations: - Check for errors - Verify field mappings working - Adjust as needed --- ## Troubleshooting **Issue**: Dropdown menus empty or missing expected fields **Solution:** 1. Click "Refresh Fields" again 2. Check Record Metadata endpoint returns field schema 3. Verify OpenAPI spec includes response schemas 4. Test metadata endpoint directly with API client 5. Review Integration Broker logs for parsing errors **Issue**: No endpoints showing in dropdown menus **Solution:** 1. Re-upload OpenAPI spec (may have failed to parse) 2. Validate spec with Swagger Editor 3. Ensure spec includes all required endpoints 4. Check spec version supported (OpenAPI 2.x/3.x) **Issue**: Cannot save mappings **Solution:** 1. Ensure all required FN fields mapped 2. Check data type compatibility 3. Add default values for optional fields 4. Review error message for specific field --- --- ### Overview URL: /docs/connectors/platforms/rest-connector/overview ## Overview The REST Connector enables integration with **any REST API** that has an OpenAPI specification: - **Upload OpenAPI spec**: System automatically discovers endpoints and data models - **Configure endpoints**: Select CRUD operations from dropdown menus - **Map fields**: Use standard field mapping UI populated from spec - **Basic Authentication**: Simple username:password credentials - **No code required**: Configuration-driven setup --- ## Key Features Works with ANY system that has an OpenAPI spec Automatically reads endpoints and models from spec Select endpoints from dropdowns, no coding needed Use familiar field mapping UI and transformations --- ## How It Works ### Upload OpenAPI Spec Upload your system's OpenAPI (Swagger) specification file (JSON or YAML) ### Configure Endpoints Select endpoints for each operation from dropdown menus: - **Record Metadata**: Get field definitions - **Record Fetch**: Retrieve single record - **Record Create**: Create new records - **Record Update**: Update existing records - **Attachments Upload**: Upload files (optional) ### Set Authentication Provide Basic Auth credentials (username:password format) ### Refresh Fields Click "Refresh Fields" to fetch field definitions from your API ### Map Fields Configure standard field mappings using discovered fields ### Test & Deploy Test with sample data, then activate for production use --- ## Architecture ```mermaid sequenceDiagram participant User as User/Admin participant Broker as Integration Broker participant Spec as OpenAPI Spec participant External as External API participant FN as Field Nation User->>Broker: Upload OpenAPI Spec Broker->>Spec: Parse Spec Spec-->>Broker: Endpoints & Models Broker->>User: Populate Dropdown Menus User->>Broker: Select Endpoints User->>Broker: Configure Auth User->>Broker: Click "Refresh Fields" Broker->>External: GET /metadata (or configured endpoint) External-->>Broker: Field Definitions Broker->>User: Populate Field Mapping UI Note over User,FN: Integration Active FN->>Broker: Work Order Event Broker->>Broker: Apply Field Mappings Broker->>External: POST /create (configured endpoint) External-->>Broker: Success + Record ID ``` --- ## When to Use REST Connector ### ✅ Use REST Connector When Your platform doesn't have a pre-built connector but has an OpenAPI spec. **Examples:** - Custom internal systems - Niche vertical applications - Proprietary Pre built connectors - Industry-specific tools Your system provides an OpenAPI/Swagger specification file. **Formats Supported:** - OpenAPI 3.x (JSON or YAML) - OpenAPI 2.x / Swagger 2.0 - Most API documentation tools generate these automatically You need standard create/read/update operations without complex workflows. **Supported:** - Create work orders → Create records in your system - Update work orders → Update records in your system - Fetch record data → Query your system's API - Upload attachments → Send files to your system **Not Supported (use REST API + Webhooks instead):** - Complex multi-step workflows - Advanced business logic - Custom validation rules - Real-time bidirectional sync requirements beyond standard operations You prefer UI configuration over custom code development. **Benefits:** - No development required - Faster deployment than custom integration - Maintained by Field Nation - Standard field mapping UI --- ### ⚠️ Limitations The REST Connector is built for **standard CRUD operations**. It may not support: > [INFO] **Extensibility**: The REST Connector was built for specific use cases and can be extended with additional functionality as needed. Contact [Field Nation Support](https://app.fieldnation.com/support-cases) to discuss feature requirements. --- ## Common Use Cases ### Custom Pre built Connector Integration Your company uses a proprietary Field Service Management system with REST API and OpenAPI spec. **Configuration:** - Upload system's OpenAPI spec - Select `/api/v1/tickets` (create) endpoint - Select `/api/v1/tickets/{id}` (fetch/update) endpoints - Map Field Nation work order fields to ticket fields - Enable bidirectional sync for status updates --- ### ERP System Integration Integrate with mid-market ERP without pre-built connector. **Configuration:** - Upload ERP's API specification - Select `/work_orders/create` endpoint - Select `/work_orders/{id}/update` endpoint - Map financial fields (cost, budget, billing codes) - Sync completion status back to ERP --- ### Vertical-Specific Application Industry-specific platform (healthcare, retail, manufacturing) with custom data models. **Configuration:** - Upload application's OpenAPI spec - Map custom fields to application's unique data structures - Use JSONNET for industry-specific transformations - Sync status and completion data bidirectionally --- ## Prerequisites ### External System Requirements **Required**: Valid OpenAPI/Swagger specification file **Format:** - OpenAPI 3.x (JSON or YAML) - OpenAPI 2.x / Swagger 2.0 (JSON or YAML) **Must Include:** - Endpoint paths and HTTP methods - Request/response schemas - Authentication schemes - Parameter definitions **How to Get:** - Most modern APIs provide this automatically - Check API documentation portal - Tools: Swagger UI, Postman, Stoplight - May be available at `/swagger.json` or `/api-docs` **Minimum Required Endpoints:** **Record Create** (Required) - Creates new records in your system - Example: `POST /api/v1/tickets` **Record Fetch** (Required) - Retrieves single record by ID - Example: `GET /api/v1/tickets/{id}` **Record Metadata** (Required) - Returns field definitions/schema - Example: `GET /api/v1/tickets/metadata` or included in spec **Record Update** (Recommended) - Updates existing records - Example: `PUT /api/v1/tickets/{id}` or `PATCH /api/v1/tickets/{id}` **Attachments Upload** (Optional) - Uploads files/attachments - Example: `POST /api/v1/tickets/{id}/attachments` **Required**: REST API must support Basic Auth **Format**: `Authorization: Basic base64(username:password)` **Other Auth Methods Not Supported:** - ❌ OAuth 2.0 (use REST API integration) - ❌ API Keys in headers (use REST API integration) - ❌ Custom authentication schemes - ❌ JWT/Bearer tokens (unless wrapped in Basic Auth) If your API doesn't support Basic Auth, consider: - Adding Basic Auth layer via API gateway - Using REST API + Webhooks integration instead **Outbound from External System** (Inbound sync) - System can send webhooks to Field Nation - Not blocked by firewall - HTTPS/TLS support **Inbound to External System** (Outbound sync) - Field Nation can reach your API endpoints - Public internet or VPN access - IP whitelisting configured (if required) [Field Nation IP addresses →](/docs/resources/environments) ### Field Nation Requirements - Active buyer account with admin access - Integration settings access - Sandbox environment (recommended for testing) --- ## Authentication REST Connector uses **Basic Authentication** only: ``` Authorization: Basic base64(username:password) ``` **Configuration:** - Enter credentials in `username:password` format - Example: `api_user:my_secure_password123` - Field Nation encodes credentials automatically - Credentials encrypted at rest (AES-256) **Best Practices:** - Use dedicated API user (not personal account) - Grant minimum required permissions - Rotate credentials periodically - Use strong, unique passwords --- ## Supported Functionality ### ✅ What REST Connector Can Do - **Create Records**: Create records in external system when FN work order created/updated - **Update Records**: Update external records when FN work order status changes - **Fetch Data**: Retrieve field values from external system for field mapping - **Field Mapping**: Standard field mappings with all transformation actions - **Custom Actions**: JSONNET transformations for complex data manipulation - **Attachments**: Upload files to external system (if endpoint configured) - **Bidirectional Sync**: Status updates flow both directions - **Correlation IDs**: Track linked records across systems ### ⚠️ What REST Connector May Not Support - OAuth or API key authentication - Complex multi-step workflows - Real-time streaming data - GraphQL queries (REST only) - SOAP web services (REST only) - Custom headers or authentication schemes - Advanced error recovery beyond standard retries For these requirements, consider [REST API + Webhooks integration](/docs/getting-started/quick-start). --- ## Comparison: REST Connector vs REST API | Feature | REST Connector | REST API + Webhooks | |---------|----------------|---------------------| | **Setup Time** | Hours | Days to weeks | | **Development** | None (config only) | Full custom development | | **Authentication** | Basic Auth only | All methods supported | | **OpenAPI Spec** | Required | Optional (helpful) | | **Customization** | Standard field mappings + JSONNET | Unlimited | | **Maintenance** | Field Nation | Your team | | **Complex Workflows** | Limited | Full control | | **Best For** | Standard CRUD operations | Custom/complex integrations | --- --- ### Workflow URL: /docs/connectors/platforms/rest-connector/workflow ## Workflow Overview ```mermaid graph TD A[Prepare OpenAPI Spec] --> B[Upload to Field Nation] B --> C[Configure Endpoints] C --> D[Set Authentication] D --> E[Test Connection] E --> F[Refresh Fields] F --> G[Configure Mappings] G --> H[Test Integration] H --> I{Success?} I -->|Yes| J[Deploy to Production] I -->|No| K[Debug & Fix] K --> H ``` --- ## Phase 1: Preparation ### Gather API Documentation ### Obtain OpenAPI Spec **Sources:** - API documentation portal (often `/docs`) - Developer resources section - API provider's GitHub repository - Generate from Postman collection - Export from API gateway **Direct URLs to Check:** ``` https://your-api.com/swagger.json https://your-api.com/api-docs https://your-api.com/openapi.yaml https://your-api.com/v1/openapi.json ``` ### Validate Specification Use online or CLI tools: ```bash # Swagger Editor (online) https://editor.swagger.io/ # CLI Validation npm install -g swagger-cli swagger-cli validate openapi.json # Or using Docker docker run --rm -v $(pwd):/spec openapitools/openapi-generator-cli validate -i /spec/openapi.json ``` **Check For:** - ☐ Valid JSON/YAML syntax - ☐ All required endpoints defined - ☐ Request/response schemas present - ☐ Parameter definitions complete - ☐ Authentication schemes documented ### Identify Required Endpoints Map your business flow to API endpoints: | Field Nation Event | External API Endpoint | Purpose | |--------------------|----------------------|---------| | Work order created | `POST /tickets` | Create record | | Work order updated | `PATCH /tickets/{id}` | Update record | | Need field schema | `GET /tickets/metadata` | Get fields | | Query record data | `GET /tickets/{id}` | Fetch record | | Attachment added | `POST /tickets/{id}/files` | Upload file | --- ### Prepare Test Credentials ### Create API User In your external system: 1. Create dedicated integration user 2. Generate API credentials 3. Grant minimum required permissions ### Document Credentials Store securely: ``` Username: api_integration_user Password: [secure password] Format for Field Nation: username:password Base URL: https://api.example.com/v1 ``` ### Test Credentials Verify with `curl`: ```bash # Test authentication curl -u "username:password" \ https://api.example.com/v1/metadata # Test endpoint access curl -u "username:password" \ https://api.example.com/v1/tickets # Test record creation (optional) curl -u "username:password" \ -X POST \ -H "Content-Type: application/json" \ -d '{"title":"Test","description":"Integration test"}' \ https://api.example.com/v1/tickets ``` --- ## Phase 2: Field Nation Configuration ### Upload & Configure ### Access REST Connector Navigate to Integration Broker: - **Sandbox**: [ui-sandbox.fndev.net/integrations](https://ui-sandbox.fndev.net/integrations) - **Production**: [app.fieldnation.com/integrations](https://app.fieldnation.com/integrations) Select "REST Connector" ### Upload OpenAPI Spec 1. Click "Upload OpenAPI Spec" 2. Select your spec file (`.json` or `.yaml`) 3. Wait for validation 4. Confirm success message ### Select Endpoints **Record Metadata:** - Select endpoint that returns field schema - Example: `GET /api/v1/metadata` **Record Fetch:** - Select endpoint to get single record by ID - Example: `GET /api/v1/tickets/{id}` **Record Create:** - Select endpoint to create new records - Example: `POST /api/v1/tickets` **Record Update:** - Select endpoint to update records - Example: `PATCH /api/v1/tickets/{id}` **Attachments Upload (optional):** - Select file upload endpoint - Example: `POST /api/v1/tickets/{id}/files` ### Enter Authentication Format: `username:password` Example: `integration_user:securePass123!` ### Test Connection Click "Test Connection" button - ✅ Success: Green confirmation - ❌ Failure: Review error, fix, retry --- ## Phase 3: Field Mapping ### Discover & Map Fields ### Refresh Fields 1. Click "Refresh Fields" button 2. Broker calls metadata endpoint 3. Fields populate in dropdown menus ### Map Inbound Fields **External System → Field Nation** Configure how external records become FN work orders: **Required Fields:** ``` External Field → FN Field Action ────────────────────────────────────────────────────────── ticket_title → title Sync ticket_description → description Sync customer_name → location.company.name Sync scheduled_date → schedule.start Date Convert priority_code → priority Array Map ``` **Optional Fields:** ``` contact_email → contact.email Sync service_address → location.address1 Sync special_instructions → instructions Sync estimated_duration → time_estimate Sync ``` ### Map Outbound Fields **Field Nation → External System** Configure how FN events update external records: **Status Updates:** ``` FN Field → External Field Action ────────────────────────────────────────────────────────── status.name → ticket_status Array Map assignee.user.name → technician_name Sync completion_notes → resolution Sync completion_date → completed_at Date Convert ``` ### Configure Transformations **Status Mapping (Array Map):** ```jsonnet { "source": "status_code", "target": "priority", "action": "array_map", "mappings": [ { "compare": "1", "value": "Low" }, { "compare": "2", "value": "Medium" }, { "compare": "3", "value": "High" }, { "compare": "4", "value": "Critical" } ], "default": "Medium" } ``` **Date Conversion:** ```jsonnet { "source": "created_date", "target": "schedule.start", "action": "date_convert", "input_format": "YYYY-MM-DD", "output_format": "YYYY-MM-DD HH:mm:ss", "timezone_in": "UTC", "timezone_out": "America/New_York" } ``` **Custom Logic (JSONNET):** ```jsonnet { "target": "custom_priority", "action": "custom", "script": ||| local status = $.util.lookup_field($.input, 'status', ''); local amount = $.util.lookup_field($.input, 'amount', 0); if status == 'urgent' && amount > 500 then "P1-Critical" else if status == 'urgent' then "P2-High" else if amount > 1000 then "P2-High" else "P3-Normal" ||| } ``` ### Save Mappings Click "Save" to persist field mapping configuration --- ## Phase 4: Testing ### Test Scenarios ### Create Test Record in Field Nation **Scenario 1: Basic Work Order Creation** 1. Create simple work order in Field Nation 2. Fill in all required fields 3. Include fields mapped to external system 4. Save work order **Expected Result:** - Integration Broker processes event - Calls external API create endpoint - Record created in external system - Correlation ID stored ### Verify External System 1. Log into external system 2. Find newly created record 3. Verify field values match: - Title/subject - Description - Priority/status - Customer/company info - Scheduled date ### Test Status Update (Outbound) **Scenario 2: Bidirectional Sync** 1. Update work order status in Field Nation 2. Add completion notes 3. Save changes **Expected Result:** - Broker processes status change event - Calls external API update endpoint - External record status updated - Notes synced to external system ### Test Inbound Sync (if configured) **Scenario 3: External → Field Nation** 1. Update record in external system 2. Trigger webhook (if configured) 3. Check Field Nation for updated data **Expected Result:** - Webhook received by Broker - Field Nation work order updated - Field changes reflected ### Test Edge Cases **Scenario 4: Error Handling** - Missing required fields - Invalid data types - Network failures - Authentication errors **Check:** - Errors logged properly - Retry logic works - Dead letter queue captures failures --- ### Review Integration Logs ### Access Logs Field Nation → Integrations → Logs ### Filter by Date/Time Set range covering your test period ### Review Entries **Success Logs:** - Event received - API called successfully - Record created/updated - Response logged **Error Logs:** - Error type and message - Request details - Response details - Retry attempts ### Common Issues **401 Unauthorized:** ``` Error: Authentication failed Solution: Verify credentials correct ``` **400 Bad Request:** ``` Error: Invalid field value for 'priority' Solution: Check Array Map values match API expectations ``` **404 Not Found:** ``` Error: Endpoint /api/v1/ticket not found Solution: Verify endpoint URL, check for typos ``` --- ## Phase 5: Production Deployment ### Pre-Launch Checklist - ☐ All sandbox tests passed - ☐ Field mappings validated - ☐ Error handling tested - ☐ Team training complete - ☐ Documentation updated - ☐ Rollback plan prepared --- ### Deploy to Production ### Recreate Configuration in Production If tested in sandbox: 1. Access production Integration Broker 2. Select REST Connector 3. Upload same OpenAPI spec 4. Configure same endpoints 5. Update credentials (production API user) 6. Test connection 7. Refresh fields 8. **Copy field mappings** from sandbox config 9. Save configuration ### Update External System Configuration If using webhooks: 1. Update webhook URL to production Field Nation 2. Verify IP whitelisting (production IPs) 3. Test webhook delivery ### Soft Launch **Start Small:** 1. Enable for single team/department 2. Monitor closely for first week 3. Collect feedback 4. Adjust configuration as needed ### Full Rollout After successful soft launch: 1. Enable for all teams 2. Communicate to stakeholders 3. Provide user training 4. Set up monitoring/alerts --- ## Monitoring & Maintenance ### Daily Monitoring - ☐ Check Integration Broker logs for errors - ☐ Review sync success rate - ☐ Monitor queue depth - ☐ Verify no authentication failures ### Weekly Tasks - ☐ Review error patterns - ☐ Test sample synchronization - ☐ Check external API status - ☐ Verify field mappings still valid ### Monthly Maintenance - ☐ Review and optimize field mappings - ☐ Update OpenAPI spec if API changed - ☐ Audit API user permissions - ☐ Test disaster recovery process ### Quarterly Reviews - ☐ Rotate API credentials - ☐ Review integration architecture - ☐ Assess performance metrics - ☐ Plan enhancements --- ## Troubleshooting Common Issues **Check:** 1. Integration active and not paused 2. Field Nation events triggering 3. External API reachable 4. Credentials valid 5. No rate limiting **Debug:** - Review Integration Broker logs - Test API endpoints with `curl` - Verify field mappings complete - Check for validation errors **Check:** 1. Field mappings configured correctly 2. Data type transformations applied 3. Array Map values match API expectations 4. Date formats compatible **Fix:** - Review and update field mappings - Add transformations (Array Map, Date Convert) - Use custom JSONNET for complex logic - Test with sample data **Check:** 1. Credentials format: `username:password` 2. Password hasn't changed 3. API user still active 4. Permissions not revoked **Solution:** - Generate new credentials - Update Field Nation configuration - Test connection - Document new credentials securely --- --- ### Configuration URL: /docs/connectors/platforms/salesforce/configuration Complete both sections in order. The Salesforce side can be handed off to your Salesforce admin to complete independently — they hand you back the credentials before you start the Field Nation side. **Have ready:** Your completed reference sheet from [Overview](/docs/connectors/platforms/salesforce/overview#your-reference-sheet). --- ## Configure Salesforce **Estimated time:** ~5 min (Path A) or ~25 min (Path B) **Access needed:** Salesforce System Administrator This section provisions the service account the connector authenticates as and grants it the minimum permissions needed to read records and sync data. If you are not the Salesforce admin, share this page with them and wait for them to hand you back the credentials before starting [Configure Field Nation](#configure-field-nation). > [INFO] **This section can be done asynchronously.** Your Salesforce admin can complete it independently and hand you back the credentials. You do not need to be present while they work. ### Choose your path | | Path A — Basic Admin | Path B — Dedicated Integration User | |---|---|---| | Setup time | ~5 minutes | ~25 minutes | | Best for | Sandboxes, one-off tests | All production environments | | Blast radius if credentials are compromised | Entire Salesforce org | Scoped to mapped objects only | | Modify Metadata exposure | Permanent | Setup-only, removed after Step 8 | | PoLP compliant | No | Yes | --- ### Path A — Basic Admin User Use this path only in non-production environments. #### Step A-1 — Create the user 1. Go to **Setup → Users → Users → New User** 2. Set the following: | Field | Value | |---|---| | First Name | `FieldNation` | | Last Name | `Integration` | | Email | A monitored service mailbox, e.g. `fn-integration@yourcompany.com` | | Username | Globally unique across all Salesforce orgs, e.g. `fn-integration@yourcompany.com.sandbox` | | Profile | `System Administrator` | 3. Click **Save** > Use a service mailbox, not a personal account. Security tokens are sent to the user's email — if the account belongs to a real person who leaves, the integration breaks. #### Step A-2 — Generate a security token 1. In the new user's record, click **Login** (or log in as the user) 2. Click the avatar → **Settings → My Personal Information → Reset My Security Token** 3. Click **Reset Security Token** 4. Check the mailbox — the token arrives within a few minutes #### Step A-3 — Record credentials ``` Username: fn-integration@yourcompany.com.sandbox Password: (set during user creation) Token: (from the reset email) Instance Type: test Instance Name: (your subdomain, from Overview) ``` **Path A complete.** Jump to [Configure Field Nation](#configure-field-nation). --- ### Path B — Dedicated Integration User (Recommended) Follow all steps below in order. --- #### Step 1 — Create a restricted profile The profile is the baseline. Using `Minimum Access - Salesforce` as the base means this profile grants near-zero permissions by default. Every permission the connector actually needs is added explicitly in Step 2 via a permission set. 1. Go to **Setup → Users → Profiles** 2. Click **New Profile** 3. Set: | Field | Value | |---|---| | Existing Profile | `Minimum Access - Salesforce` | | Profile Name | `FieldNation Integration Profile` | 4. Click **Save** > [INFO] Do not add object or field permissions directly to this profile. All access is granted through the permission set in Step 2 — this separation makes it easy to audit and adjust permissions without touching the profile. ✓ **Verify:** The new profile appears in **Setup → Users → Profiles** with base access only. --- #### Step 2 — Create a permission set Permission sets layer explicit access on top of the restricted profile. This approach lets you grant exactly what the connector needs — nothing more. ##### 2a — Create the permission set 1. Go to **Setup → Users → Permission Sets → New** 2. Set: | Field | Value | |---|---| | Label | `FieldNation Integration` | | API Name | `FieldNation_Integration` *(auto-populated from the Label — do not modify this value)* | | License | `Salesforce` *(this field appears at the bottom of the New Permission Set form — set it before clicking Save)* | 3. Click **Save** ##### 2b — System permissions 1. From the permission set, click **System Permissions → Edit** 2. Enable the following: | Permission | Enable | Remove after setup? | |---|:---:|:---:| | API Enabled | ✓ | No — required at runtime | | Modify Metadata Through Metadata API Functions | ✓ | **Yes** — remove after setup is confirmed | > [INFO] **Modify Metadata Through Metadata API Functions** is disabled by default in the permission set — you must explicitly enable it here. 3. Click **Save** > [INFO] **Why Modify Metadata?** The connector automatically creates a Salesforce Workflow Rule and Outbound Message during initial configuration in Configure Field Nation. This is a one-time operation. Step 9 of this section walks you through removing this permission after setup is confirmed. ##### 2c — Set object permissions After saving the permission set, return to its main page (via **Setup → Users → Permission Sets → FieldNation Integration**) and click **Object Settings** from the menu on the left. 1. From the created permission set, click **Object Settings** 2. Configure each object below. Only enable what applies to your use case: > [INFO] The tables below show the **display names** as they appear in the Salesforce Object Settings UI, followed by the API name in parentheses. The UI shows objects by display name — use the search bar to locate them quickly. **Always required:** | Object (display name) | API name | Read | Create | Edit | Notes | |---|---|:---:|:---:|:---:|---| | Your root object (e.g., **Cases**) | e.g., `Case` | ✓ | ✓ | ✓ | Remove Create if Outbound Create feature is off | | **Contacts** | `Contact` | ✓ | | | Reference field lookups | | **Accounts** | `Account` | ✓ | | | Reference field lookups | **Required if the Messages feature will be enabled:** | Object (display name) | API name | Read | Create | Notes | |---|---|:---:|:---:|---| | **Case Comments** | `CaseComment` | ✓ | ✓ | Default for Case root objects. If Case Comments does not appear in Object Settings, search for "Case Comment" — in some orgs this object is inherited from Case-level access and will not show up as a standalone entry. | | **Notes** (Classic) | `Note` | ✓ | ✓ | Use if root object is not Case. Select the entry labeled **Notes** — not **Notes & Attachments** or **ContentNote**, which are separate objects. | **Required if using classic Attachments (`attachments` type):** > [INFO] In Object Settings, locate **Attachments** (`Attachment`). The object-level row shows **Read** and **Create** checkboxes — enable both. If you only see **Available** and **Visible** toggles (with no Read/Create checkboxes), your org has deprecated classic Attachments in favor of Salesforce Files — skip this section and use the Salesforce Files permissions below. | Object (display name) | API name | Read | Create | |---|---|:---:|:---:| | **Attachments** | `Attachment` | ✓ | ✓ | **Required if using Salesforce Files (`files` type):** > [INFO] These objects may not be visible when scrolling the Object Settings list. Use the search bar at the top of the Object Settings page to locate each one by display name: search `Content Version`, `Content Document`, and `Content Document Link`. | Object (display name) | API name | Read | Create | |---|---|:---:|:---:| | **Content Versions** | `ContentVersion` | ✓ | ✓ | | **Content Documents** | `ContentDocument` | ✓ | | | **Content Document Links** | `ContentDocumentLink` | ✓ | ✓ | **Required only for explicitly mapped custom objects:** > [INFO] Custom objects appear in Object Settings by their display label (not the `__c` API name). Use the search bar to locate each one. If a custom object does not appear, verify it exists under **Setup → Object Manager** and that the integration user's profile or license supports access to it. | Object | API name | Read | Create | Edit | |---|---|:---:|:---:|:---:| | Each mapped custom object (by display label) | e.g., `CustomObject__c` | ✓ | ✓ | ✓ | > [INFO] No Delete permissions are needed anywhere. The connector has no delete operations. ##### 2d — Field-level security For each object above, open its **Field Permissions** within the permission set and grant **Read** (and **Edit** where the object has edit access) only on fields you intend to map. Leave all other fields with no access. > [INFO] **Order of operations:** You configure field mappings in Configure Field Nation after this section is complete. If you are not sure which fields to include right now, grant Read on all fields for the relevant objects and return here after Configure Field Nation to tighten access to only what you mapped. Configure Field Nation will show you a dropdown of available fields — anything not appearing there needs Read access granted here. ✓ **Verify:** The permission set appears under **Setup → Users → Permission Sets** with System Permissions and Object Settings configured. --- #### Step 3 — Create the integration user 1. Go to **Setup → Users → Users → New User** 2. Fill in the following fields: - **First Name:** `FieldNation` - **Last Name:** `Integration` - **Email:** A monitored service mailbox — e.g. `fn-integration@yourcompany.com` - **Username:** Must be globally unique across all Salesforce orgs — e.g. `fn-integration@yourcompany.com.prod` - **User License:** `Salesforce` - **Profile:** `FieldNation Integration Profile` 3. Click **Save** > Use a service mailbox your team monitors. The security token and any authentication alerts are sent to this address. ✓ **Verify:** The user appears in the Users list with the `FieldNation Integration Profile` profile. --- #### Step 4 — Assign the permission set 1. Open the integration user's record 2. Scroll to **Permission Set Assignments** 3. Click **Edit Assignments** 4. Move `FieldNation Integration` from **Available** to **Enabled Permission Sets** 5. Click **Save** ✓ **Verify:** `FieldNation Integration` appears under Permission Set Assignments on the user record. --- #### Step 5 — Generate a security token The connector authenticates using the format `password + security_token` internally. You enter them as separate values in Field Nation. The token changes each time the password is reset — if you rotate the password, you must also update the token in Configure Field Nation. 1. Log in as the integration user, or go to **Setup → Users**, open the user, and click **Login** 2. Click the avatar → **Settings → My Personal Information → Reset My Security Token** 3. Click **Reset Security Token** 4. Check the service mailbox — the token arrives within a few minutes 5. Copy the token and store it securely alongside the password ✓ **Verify:** You received an email to the service mailbox with a subject like "Your new Salesforce security token." **Token email not arriving?** Check the spam or junk folder in the service mailbox. If it is not there after 10 minutes, confirm the mailbox address on the user record is correct under **Setup → Users**, then click **Reset Security Token** again. If the mailbox uses routing rules, verify delivery to the correct folder. > [INFO] **Sandbox and production use separate tokens.** If you are setting up both environments, generate and record a separate token for each integration user. Tokens are not shared between Salesforce orgs. --- #### Step 6 — Handle MFA (if your org enforces it) If your Salesforce org requires MFA for all users, the integration user must be exempted. API-only integrations cannot complete an interactive MFA challenge. **Check if MFA is enforced:** Go to **Setup → Identity → Identity Verification**. If "Require MFA for all direct UI logins" is enabled org-wide, proceed with one of the options below. **Option 1 — Permission set exemption (preferred):** 1. Go to **Setup → Users → Permission Sets → New** 2. Set **Label** to `MFA Exemption - API Only`, **API Name** to `MFA_Exemption_API_Only` 3. Under **System Permissions**, enable: `Waive Multi-Factor Authentication for Exempt Users` 4. Save, then assign this permission set to the integration user (same process as Step 4) **Option 2 — Org-level opt-out:** If Salesforce has granted your org an MFA opt-out waiver, navigate to **Setup → Identity → MFA Opt-Out**. This affects all users org-wide and is not recommended unless Option 1 is unavailable. ✓ **Verify:** The integration user can authenticate via API without being prompted for MFA. --- #### Step 7 — Configure your trigger mechanism *(optional)* When you save in Configure Field Nation, the connector automatically creates a **Salesforce Workflow Rule** and **Outbound Message** that fires for all new or updated records on your work order resource. **If that default behavior is what you want, skip this step.** If you want more precise control — for example, dispatch only when a user checks a box or when a status field reaches a specific value — set up a trigger field and a **Record-Triggered Flow** now, before Configure Field Nation. ##### Choose a trigger approach | Approach | How it works | Best for | |---|---|---| | **Checkbox trigger** | Work order created when the user checks a custom checkbox | Teams that want explicit per-record control | | **Status trigger** | Work order created when a picklist field reaches a specific value, e.g. `Status = "Dispatch Required"` | Automated workflows where a status change is the dispatch signal | ##### Create a trigger field (checkbox approach) 1. Go to **Setup → Object Manager → [Your root object] → Fields & Relationships → New** 2. Select **Checkbox** as the field type 3. Set: | Field | Value | |---|---| | Field Label | `Send to Field Nation` | | API Name | Auto-populated, e.g. `Send_to_Field_Nation__c` | | Default Value | Unchecked | 4. Set field-level security to grant access to the profiles and users who will dispatch work 5. Add the field to relevant page layouts ##### Create a Record-Triggered Flow with conditions If you want to use a Flow instead of the auto-created Workflow Rule: Go to **Setup → Flows → New Flow** and select **Record-Triggered Flow** Configure the trigger: - **Object:** Your root object (e.g. `Case`) - **Trigger:** A record is created or updated - **Entry Conditions:** Add the condition for your trigger field — e.g. `Send_to_Field_Nation__c Equals True` - **Optimize for:** Actions and Related Records Add an **Action** element: - Click **+** after the Start element → select **Action** - Scroll to **Outbound Message** and select the Outbound Message the connector created in Configure Field Nation If completing Configure Salesforce before Configure Field Nation (recommended), you will return here to connect the action after Configure Field Nation generates the Outbound Message. **Save** the Flow with a descriptive label, e.g. `Create Field Nation Work Order` Click **Activate** — the Flow will not run until it is Active > If you create a custom Flow, disable or delete the Workflow Rule the connector auto-created in Configure Field Nation to avoid duplicate work order creation. --- #### Step 8 — Record credentials Fill in your reference sheet from Overview with the values from this section: ``` Username: fn-integration@yourcompany.com.prod Password: (set during user creation) Token: (from the reset email in Step 5) Instance Type: production (or: test) Instance Name: (your subdomain, from Overview) ``` --- #### Step 9 — Post-setup cleanup *(complete after Configure Field Nation)* > Return here after Configure Field Nation is complete and the connection is confirmed working. Once the Salesforce Workflow Rule and Outbound Message have been created automatically during Configure Field Nation, the Modify Metadata permission is no longer needed: 1. Go to **Setup → Users → Permission Sets → FieldNation Integration** 2. Click **System Permissions → Edit** 3. Uncheck `Modify Metadata Through Metadata API Functions` 4. Click **Save** This removes the ability to create or modify Salesforce metadata from the integration account. All ongoing runtime operations continue to work without it. - [ ] Step 9 cleanup completed after Configure Field Nation *(mark when done)* ✓ **Verify:** The permission is no longer checked under System Permissions. --- ### Salesforce troubleshooting | Problem | Likely cause | Fix | |---|---|---| | "Login failed" when testing in Configure Field Nation | Wrong username, password, or token | Re-verify all three; remember the token changes on every password reset | | Cannot create the Workflow Rule during Configure Field Nation | Modify Metadata permission not granted | Verify [Step 2b](#2b--system-permissions) — System Permissions | | Fields not visible in Field Nation mapper | Field-level security too restrictive | Return to [Step 2d](#2d--field-level-security) and grant Read on the missing field | | Authentication fails despite correct credentials | IP restriction on the org | Add Field Nation's egress IPs to the trusted IP range under **Setup → Network Access** | | MFA challenge appears during API call | MFA exemption not applied | Complete [Step 6](#step-6--handle-mfa-if-your-org-enforces-it) | | Flow not triggering | Flow is not Active, or entry conditions not met | Check **Setup → Flows** — status must be Active; verify the trigger field is set correctly on the record | | Duplicate work orders being created | Both Workflow Rule and custom Flow are active | Deactivate the auto-created Workflow Rule if using a custom Flow | If none of the above resolves your issue, [contact Field Nation Support](https://support.fieldnation.com). Include the exact error message, which step you are on, and your Salesforce instance type (`production` or `test`). --- ## Configure Field Nation **Estimated time:** ~35 minutes **Access needed:** Field Nation Company Admin **Have ready:** Reference sheet from [Overview](/docs/connectors/platforms/salesforce/overview#your-reference-sheet) and credentials from [Step 8](#step-8--record-credentials) above Install the Salesforce connector in Field Nation, authenticate it against your Salesforce org, map fields, enable sync features, and run end-to-end tests to confirm bidirectional data flow. By the end of this section the integration will be live. --- ### Step 1 — Install the connector 1. Log in to Field Nation as a Company Admin and navigate to your environment: | Environment | URL | |---|---| | Production | `app.fieldnation.com/integrations` | | Sandbox | `ui-sandbox.fndev.net/integrations` | 2. Go to **Marketplace** 3. Find **Salesforce** in the connector list 4. Click **Install** (first time) or **Configure** (if already installed) ✓ **Verify:** You land on the Salesforce connector configuration page with a Settings tab visible. > [INFO] **Sandbox and production are fully isolated.** Credentials, Trigger URLs, field mappings, and feature settings are not shared between environments. If you are validating in sandbox first, you will repeat this entire section for production using separate Salesforce credentials and a separate Trigger URL. --- ### Step 2 — Basic settings ![Salesforce Settings](./images/salesforce_settings.webp) Fill in the identity fields for your Salesforce org. These tell the connector where to connect and which objects to work with. | Field | Your value | Notes | |---|---|---| | **Instance Name** | *(from Overview)* | The subdomain of your Salesforce URL, e.g. `myInstance`. Used as a label — does not affect authentication. | | **Instance Type** | `production` or `test` | `production` connects to `login.salesforce.com`. `test` connects to `test.salesforce.com`. | | **Object Name** | *(from Overview, e.g. `Case`)* | The API name of the root Salesforce object. Custom objects use the `__c` suffix. | | **Message Object Name** | *(from Overview, e.g. `CaseComment`)* | The child object used for work order messages. | | **Message Object Body Field Name** | *(from Overview, e.g. `CommentBody`)* | The field on the message object that holds the message text. | > Instance Type must match your Salesforce org exactly. Connecting a `test` instance type to a production Salesforce org — or vice versa — will fail authentication even if the credentials are correct. ### Message object reference The message object stores comments synced between Field Nation work orders and Salesforce. Common values: | Root object | Message object | Body field | |---|---|---| | `Case` | `CaseComment` | `CommentBody` | | Chatter-enabled objects | `FeedComment` | `Body` | | Any other object | `Note` | `Body` | Field Nation connects to Salesforce standard comment objects. Custom comment objects are not supported. Enable messaging if you want provider messages from Field Nation to appear in Salesforce, or Salesforce user comments to sync to Field Nation work orders. Common use cases include dispatchers adding notes for technicians and technicians providing status updates visible in Salesforce. --- ### Step 3 — Credentials Enter the Salesforce integration user credentials from Configure Salesforce above. | Field | Value | Security note | |---|---|---| | **User Name** | Full email address of the integration user | Stored as the identifier | | **Password** | Integration user's password | Encrypted at rest with AES-256 | | **Token** | Security token from [Step 5](#step-5--generate-a-security-token) above | Encrypted at rest — entered separately, appended to password by the connector internally | > [INFO] The Token field is separate from the Password field. Do not concatenate them yourself. Enter each value independently — the connector handles the combination. > The security token changes every time the Salesforce user's password is reset. If you rotate the password in Salesforce, you must update both the Password and Token fields here. **Optional:** | Field | Value | Notes | |---|---|---| | **Notification Email Addresses** | Comma-separated list | Recipients of failure alerts when a transaction cannot be processed | | **Salesforce Attachment Type** | `attachments` or `files` | Defaults to `attachments`. Set to `files` if your org uses Salesforce Files (ContentVersion). | --- ### Step 4 — Test connection Click **Save**. The connector will attempt to authenticate against Salesforce using the credentials you entered. **On success:** A green confirmation message appears and a unique **Trigger URL** is generated. Continue to Step 5. **On failure:** An error message describes the problem. Refer to the table below. | Error message | Cause | Fix | |---|---|---| | `INVALID_LOGIN: Invalid username, password, security token` | Wrong username, password, or outdated token | Verify all three. The token resets every time the password changes. | | Connection timeout or auth failure | Instance type doesn't match Salesforce environment | Change Instance Type to match your org (`production` vs `test`) | | `API_DISABLED_FOR_ORG` | API Enabled permission not granted | Return to [Step 2b](#2b--system-permissions) | | `LOGIN_MUST_USE_SECURITY_TOKEN` or login failure | Field Nation IPs not in the org's trusted IP range | Add them under **Salesforce Setup → Network Access** | | MFA challenge | MFA exemption not applied to the integration user | Complete [Step 6](#step-6--handle-mfa-if-your-org-enforces-it) | ✓ **Verify:** Green confirmation message is visible and the page shows a Trigger URL. --- ### Step 5 — Record the Trigger URL After a successful connection test, Field Nation generates a unique Trigger URL: ``` https://api.fieldnation.com/integrations/trigger/{CLIENT_TOKEN} ``` This is the endpoint where Salesforce sends Outbound Message payloads. The connector also uses its Modify Metadata permission at this point to automatically create a **Salesforce Workflow Rule** and **Outbound Message** on your configured root object — no manual Salesforce setup required. - [ ] Trigger URL recorded and stored securely > Treat this URL as a secret. Anyone with this URL can send payloads to your Field Nation integration. Do not commit it to version control or share it in public channels. > [INFO] If you set up a custom Record-Triggered Flow in [Step 7](#step-7--configure-your-trigger-mechanism-optional), return to that Flow now and connect the Outbound Message action. Then deactivate the Workflow Rule the connector auto-created to avoid duplicate work order creation. --- ### Step 6 — Map fields Field mappings control which Salesforce fields populate which Field Nation work order fields. Navigate to each section in the left sidebar and configure mappings relevant to your workflow. #### How to map a field 1. In the integration sidebar, click a mapping section (e.g., **Overview**) 2. Click **Refresh Fields** to pull the current field schema from your Salesforce org — this queries the Salesforce Metadata API and discovers all standard fields, custom fields, related object fields (e.g. `Account.Name`, `Contact.Email`), formula fields, and picklist fields 3. For each Field Nation field, select the corresponding Salesforce field from the dropdown 4. Only fields the integration user has Read access to appear in the dropdown > [INFO] If a field is missing from the dropdown, the integration user's field-level security does not include Read access on that field. Return to [Step 2d](#2d--field-level-security) and grant access, then click Refresh Fields again. #### Available mapping sections | Section | What it populates | |---|---| | **Overview** | Work order title, type, description | | **Schedule** | Service date and time window | | **Location** | Site address, city, state, zip | | **Pay** | Pay rate and rate type | | **Expense** | Expense allowance | | **Contacts** | Manager and on-site contact | | **Service Description** | Scope of work text | | **Client Documents** | Imported attachments | | **Tasks** | Work order checklist items | | **Buyer Custom Fields** | Buyer-defined custom field values | | **Provider Custom Fields** | Provider-facing custom field values | | **Assigned Provider** | Preferred or required technician | | **Status Updates** | Lifecycle event mappings | | **Time Log** | Check-in and check-out times | | **Deliverables** | Completion uploads and signatures | | **Finance** | Invoice and payment fields | | **Dispatch** | Auto-dispatch configuration | | **Messages** | Work order message thread | | **Shipments** | Shipment tracking data | | **Job Summary** | Completion summary | > [INFO] Map only the sections your workflow uses. Unmapped sections are ignored — you are not required to configure all of them. --- ### Step 7 — Enable features Navigate to **Settings** in the integration and enable the features that match your workflow. Each feature has a corresponding Salesforce permission requirement. | Feature | What it does | Salesforce permission needed | |---|---|---| | **Auto Dispatch** | Automatically dispatches the work order on creation | None beyond base permissions | | **Attachments Import** | Pulls files from the Salesforce Case into Client Documents when the work order is created | `Attachment` or `ContentVersion`: Read | | **Attachments Export** | Pushes provider-uploaded files back to Salesforce | `Attachment` or `ContentVersion` + `ContentDocumentLink`: Create | | **Work Order Signatures** | Sends completion signatures to Salesforce as images | Same as Attachments Export | | **Access Control (Attachments)** | Sets visibility (`public`/`private`) on attachments uploaded to Salesforce | Same as Attachments Export | | **Messages** | Syncs the work order message thread to and from Salesforce | `CaseComment` or `Note`: Read, Create | | **Access Control (Inbound Messages)** | Sets visibility for messages sent to Salesforce | Same as Messages | | **Access Control (Outbound Messages)** | Sets visibility for messages received from Salesforce | Same as Messages | | **Outbound Create** | Creates a Salesforce Case when a Field Nation work order is created | `Case` (or root object): Create | | **Inbound UTC as UTC** | Treats inbound UTC service dates as UTC instead of converting to local time | None | | **Saved Location Setting** | Controls whether to reuse saved locations or allow duplicates | None | > Only enable features you intend to use. Enabling a feature without the corresponding Salesforce permission will cause that operation to fail silently — visible in Event History. --- ### Step 8 — Save and run end-to-end tests Click **Save** to finalize the configuration, then run both tests below to confirm bidirectional sync is working. #### Inbound test — Salesforce → Field Nation 1. In your Salesforce org, create a new Case (or update one if your workflow triggers on updates) 2. The Outbound Message fires automatically — allow up to 60 seconds for delivery 3. In Field Nation, go to **Integrations → Event History** 4. Confirm a `create_external` event appears with a `success` status 5. Confirm a new work order was created in Field Nation with the correct field values #### Outbound test — Field Nation → Salesforce 1. In Field Nation, open the work order created by the inbound test 2. Perform one or more of the following depending on which features you enabled: update the work order status, add a message, or upload a file 3. Check the originating Salesforce Case — the corresponding field, comment, or attachment should appear within a few seconds #### Interpreting Event History Go to **Integrations → Event History** to see the log of all integration events. | Status | Meaning | |---|---| | `success` | Event processed and synced successfully | | `failed` | Event received but processing failed — expand the row for the error detail | | `pending` | Event queued, not yet processed | > [INFO] Each work order sync consumes 2–3 Salesforce API calls. Monitor daily API usage under **Salesforce Setup → Company Information** if you have high work order volume. Enterprise orgs have a minimum of 1,000 calls/day; Unlimited orgs have 5,000. ✓ **Verify:** Both the inbound and outbound tests show `success` in Event History. --- ### Step 9 — Remove Modify Metadata from Salesforce Now that the connection is confirmed, return to Salesforce and remove the setup-only permission: 1. Go to **Setup → Users → Permission Sets → FieldNation Integration** 2. Click **System Permissions → Edit** 3. Uncheck `Modify Metadata Through Metadata API Functions` 4. Click **Save** This is [Step 9 of Configure Salesforce](#step-9--post-setup-cleanup-complete-after-configure-field-nation). > If you skip this step, the integration user retains the ability to create, modify, or delete Salesforce metadata — including Workflow Rules, Outbound Messages, and Apex classes — through the API. This is a permanent elevated privilege until explicitly revoked. Remove it now that setup is confirmed. --- ### Configuration complete Use this checklist to track progress. Completed stages collapse automatically — click any item's arrow icon to jump directly to that step. **Next:** [Workflow Setup](/docs/connectors/platforms/salesforce/workflow) — Create Salesforce Flows and Outbound Messages. --- ### Field Nation troubleshooting Click **Refresh Fields** to re-query Salesforce. If the field still does not appear, verify the field exists on the object (check the API name in Object Manager) and confirm the integration user has Read permission on that field in the `FieldNation Integration` permission set. Check **Integrations → Event History** for a `failed` event with an error detail. Common causes: the Workflow Rule is inactive, the trigger conditions on your custom Flow are not met, or the Outbound Message is pointing to the wrong Trigger URL. The most common cause is a Salesforce password reset, which also resets the security token. Re-enter both the Password and Token fields in Step 3, then click Save to retest. Click **Refresh Fields** to pick up any schema changes. Verify data type compatibility between the Salesforce field and the Field Nation field. For picklist fields, confirm the array map values match the Salesforce picklist API values exactly. The integration user lacks permission on a specific object or field that a feature is trying to write to. Identify the object from the error detail, then return to [Step 2c](#2c--object-permissions) and add the required permission. [Contact Field Nation Support](https://support.fieldnation.com). Include the following: - The exact error message from Event History (expand the failed event row to copy it) - Your Salesforce instance type (`production` or `test`) and Field Nation environment - Which step you completed last and what you have already tried --- ### Overview URL: /docs/connectors/platforms/salesforce/overview Automate field service dispatch directly from Salesforce. When a Case record meets your conditions, the connector creates a Field Nation work order and keeps both systems in sync — no polling, no manual hand-off. Username + Password + Security Token Any standard or custom Salesforce object Outbound Messages → Field Nation Trigger URL Bidirectional — status, messages, attachments --- ## How it works The connector runs two flows: **inbound** (Salesforce creates a work order in Field Nation) and **outbound** (Field Nation pushes updates back to Salesforce). ```mermaid sequenceDiagram participant SF as Salesforce participant Broker as Integration Broker participant FN as Field Nation Note over SF,FN: Inbound — Salesforce → Field Nation SF->>Broker: Outbound Message (record ID) Broker->>SF: GET full record + related objects SF-->>Broker: Case fields, Account, Contact Broker->>Broker: Apply field mappings Broker->>FN: Create work order FN-->>Broker: Work order ID (stored as correlation ID) Note over SF,FN: Outbound — Field Nation → Salesforce FN->>Broker: Work order event (status / message / file) Broker->>SF: Update Case status + add CaseComment SF-->>Broker: Confirmation ``` --- ## Configuration stages ![Salesforce connector setup journey — four stages from Prerequisites through Test & Validate](./images/setup-journey.webp) --- ## Use cases **Automatically create a work order when a Case reaches a specific status.** The most common pattern. A record-triggered Outbound Message fires when a Case status changes to a value like "On-site Required" — no Salesforce user action needed. ```mermaid flowchart LR A["Case Status\n= Dispatch Required"] --> B["Salesforce\nOutbound Message"] B --> C["Integration Broker\nfetches full record"] C --> D["Work Order\ncreated in Field Nation"] D --> E["Correlation ID\nstored on Case"] ``` **Best for:** IT service desks, facilities teams, and field service orgs where a status change is the unambiguous dispatch signal. **Let Salesforce users decide when to dispatch — field by field.** A custom checkbox on the Case gives your team explicit control. Only Cases with the checkbox set to `true` trigger the connector. Useful when not every Case becomes a work order. ```mermaid flowchart LR A["'Send to Field Nation'\n= checked by user"] --> B["Salesforce\nOutbound Message"] B --> C["Integration Broker\nfetches full record"] C --> D["Work Order\ncreated in Field Nation"] D --> E["Checkbox reset\nor Case status updated"] ``` **Best for:** Account managers or dispatchers who review Cases before sending to the field. **Reflect work order lifecycle events back to Salesforce in real time.** When a provider completes, checks in, or is assigned in Field Nation, the Integration Broker calls the Salesforce API and updates the originating Case — no polling required. ```mermaid flowchart LR A["Work Order event\nin Field Nation"] --> B["Integration Broker"] B --> C["Case Status\nupdated in Salesforce"] B --> D["CaseComment added\nwith event details"] B --> E["Attachments / files\npushed to Case"] ``` **Configurable events:** provider assigned, provider checked in, work order completed, work order approved, work order cancelled. **Keep Salesforce users and field providers in the same conversation.** Messages added in the Field Nation work order appear as CaseComments in Salesforce. CaseComments added in Salesforce flow back into the work order message thread. ```mermaid flowchart LR A["Provider message\nin Field Nation"] --> B["Integration Broker"] B --> C["CaseComment\ncreated in Salesforce"] D["Salesforce user\nadds CaseComment"] --> E["Outbound Message"] E --> B B --> F["Message appears\nin FN work order"] ``` **Best for:** Enterprise accounts where back-office teams triage in Salesforce while technicians work in Field Nation. --- ## The setup journey | Section | What happens | Output | |---|---|---| | **Overview** *(this page)* | Understand the architecture, gather values, and make security decisions | Reference sheet filled in; decisions made | | **[Configuration](/docs/connectors/platforms/salesforce/configuration)** | Provision the integration user in Salesforce, then install and configure the connector in Field Nation | Confirmed bidirectional sync | | **[Workflow Setup](/docs/connectors/platforms/salesforce/workflow)** | Create Salesforce Flows and Outbound Messages to trigger work order creation | Integration actively dispatching work orders | **Estimated total time:** ~55 minutes for a first-time setup. > [INFO] **Configure Salesforce can be done asynchronously.** If your Salesforce admin is a separate person, send them the [Configuration](/docs/connectors/platforms/salesforce/configuration) page now. They can complete the Salesforce side independently and hand you back the credentials. You do not need to be present while they work. --- ## Your setup checklist Track every step across all three pages. Progress is saved in your browser and shared between Overview, Configuration, and Workflow Setup. --- ## Prerequisites **Estimated time:** ~10 minutes. Collect the values you'll need during configuration — no configuration yet. If you are not a Salesforce administrator, identify who is and share the [Configuration](/docs/connectors/platforms/salesforce/configuration) page with them before you start. ### What you'll need from Salesforce Gather these values from your Salesforce org. Log in as a Salesforce administrator to find them. #### 1. Instance name **Why you need this:** The connector uses the instance name to build the Salesforce API endpoint. An incorrect value causes authentication to fail even with correct credentials. Your instance name is the subdomain of your Salesforce URL. ``` https://myInstance.salesforce.com ^^^^^^^^^^ This part is your instance name ``` For sandbox orgs the URL pattern is `myInstance.sandbox.salesforce.com` — the instance name is still the first part. **Can't find it?** Log in to Salesforce and look at the URL in your browser address bar. The subdomain before `.salesforce.com` or `.sandbox.salesforce.com` is your instance name. If you see `lightning.force.com`, go to **Setup → Company Settings → Company Information** — the Instance field shows it. --- #### 2. Instance type **Why you need this:** `production` authenticates against `login.salesforce.com` and `test` against `test.salesforce.com`. Mixing them causes authentication failure even with correct credentials. | Your org is... | Instance type to use | |---|---| | A live production org | `production` | | A developer, partial copy, or full sandbox | `test` | > [INFO] Always validate in a sandbox first. Set instance type to `test` for your initial setup, then repeat with `production` once the integration is confirmed working. --- #### 3. Work order resource API name **Why you need this:** The connector watches this Salesforce object for new or updated records and creates a corresponding Field Nation work order. Using the wrong object name means no work orders are ever created. The work order resource is the Salesforce object that triggers work order creation. In most deployments this is `Case`. To confirm the API name if you are using a custom object: 1. Go to **Setup → Object Manager** 2. Find your object 3. Look at the **API Name** column — custom objects end in `__c` **Can't find it?** Ask your Salesforce admin which object your team uses to track field service requests. If you use standard Salesforce Field Service, it is typically `WorkOrder`. --- #### 4. Messaging resource API name **Why you need this:** The messaging resource stores comments synced between Field Nation work orders and Salesforce. Without it, messages stay siloed in each system. | Work order resource | Typical messaging resource | Body field name | |---|---|---| | `Case` | `CaseComment` | `CommentBody` | | Any other object | `Note` | `Body` | **Can't find it?** If you are unsure which object your org uses for comments, ask your Salesforce admin. Custom messaging objects are not supported — the connector only works with standard Salesforce comment objects. --- #### 5. Attachment type **Why you need this:** The connector uses different Salesforce API objects to read and write files depending on which storage model your org uses. Setting the wrong type means file sync silently does nothing. | Type value | Salesforce object | When to choose | |---|---|---| | `attachments` | Classic `Attachment` object | Older orgs still using legacy attachments | | `files` | `ContentVersion` + `ContentDocument` | Salesforce Files (recommended for API v37+) | **Can't find it?** Go to **Setup → Files Settings** — if "Allow users to upload files to related records" is enabled, your org uses Salesforce Files (`files`). If you are still unsure, ask your Salesforce admin which object stores files attached to your work order resource records. --- #### 6. Salesforce admin access You will need a Salesforce account with permission to create users and profiles, create and assign permission sets, and read/write metadata. **Don't have this access?** Identify your Salesforce System Administrator and share the [Configuration](/docs/connectors/platforms/salesforce/configuration) page with them now. The Salesforce side can be completed independently — they hand you back the credentials when done. --- ### What you'll need from Field Nation #### 7. Field Nation Company Admin access You will need a Field Nation account with **Company Admin** permissions to navigate to **Integrations → Marketplace**, install, and configure the Salesforce connector. --- #### 8. Field Nation environment | Environment | URL | |---|---| | Production | `app.fieldnation.com` | | Sandbox | `ui-sandbox.fndev.net` | > [INFO] If your Salesforce instance type is `test`, use the Field Nation sandbox environment. Mixing a Salesforce sandbox with Field Nation production is possible but not recommended for initial setup. --- ### Your reference sheet Fill this in as you work through each item above. Non-secret values are saved in your browser; the password and security token are masked and never saved — enter them directly into the connector. Use **Copy all** before clearing browser data or to hand off to a colleague. --- ## Modify Metadata permission lifecycle The connector requires the **Modify Metadata Through Apex and the API** permission to automatically create the Salesforce Outbound Message and Workflow Rule during initial configuration. This is a one-time operation — the permission is not needed for ongoing sync. The recommended approach: Grant the permission before starting Configure Salesforce Complete Configure Salesforce and Configure Field Nation Remove the permission after confirming the connection works The Configuration page includes an explicit cleanup step for this. Acknowledge it now so it is not skipped later. > If you do not remove this permission after setup, the integration user retains the ability to create, modify, or delete Salesforce metadata — including Workflow Rules, Outbound Messages, and Apex classes — through the API. This is a permanent elevated privilege until explicitly revoked. --- ## Terminology The connector maps between Field Nation and Salesforce. The Salesforce side varies by org. Fill in your values here — these terms appear throughout the Configuration and Workflow sections. | Term in this guide | What it refers to | Common values | Your value | |---|---|---|---| | **Work order resource** | The Salesforce record type that triggers a work order in Field Nation when created or updated | `Case` (most orgs), `Service_Appointment__c`, or a custom object ending in `__c` | | | **Messaging resource** | The Salesforce object that stores comments and notes synced with Field Nation work orders | `CaseComment` (for Case roots), `Note` (for all other root objects) | | | **Integration user** | A dedicated Salesforce service account created solely for this connector — not a real person's account | — | Created in Configure Salesforce | --- ## Key concepts ### What is a security token? A security token is a system-generated string Salesforce appends to a user's password for API calls made from IP addresses outside the org's trusted range. It is separate from the password and changes every time the password is reset. You enter it as a standalone value in Configure Field Nation — the connector appends it to the password internally. It is not optional. ### What is Least Privilege (PoLP)? The integration user should only have access to the exact objects and fields the connector needs — nothing more. This limits the damage if the credentials are ever compromised. The Configuration page walks through applying this precisely. ### What are Salesforce API limits? Salesforce counts every API call against a daily limit: | Edition | Daily API call limit | |---|---| | Enterprise | 1,000 minimum | | Unlimited | 5,000 minimum | Each work order sync consumes **2–3 API calls**. Monitor usage under **Salesforce Setup → Company Information** if you have high work order volume. --- ## Troubleshooting Log in to Salesforce and look at the URL in your browser. The subdomain before `.salesforce.com` is your instance name. If the URL shows `lightning.force.com`, go to **Setup → Company Settings → Company Information** and check the Instance field. Ask your Salesforce admin which object your team uses to track field service requests or dispatch jobs. Common values are `Case`, `WorkOrder`, or a custom object ending in `__c`. You can also go to **Setup → Object Manager** and scan the object list for anything service-related. Go to **Setup → Files Settings**. If "Allow users to upload files to related records" is enabled, your org uses Salesforce Files (`files`). Otherwise use `attachments`. If you are still unsure, ask your Salesforce admin which API object stores files on your work order resource records. You cannot proceed without Salesforce admin involvement. Share the [Configuration](/docs/connectors/platforms/salesforce/configuration) page with your admin and ask them to complete the Salesforce side. It takes ~25 minutes and can be done independently — they hand you back the credentials when finished. If none of the above resolves your issue, [contact Field Nation Support](https://support.fieldnation.com) with a description of what you are trying to find and which step you are on. --- ## Need help? If you run into an issue that the section-specific troubleshooting content does not resolve, contact Field Nation support. Include: - Which section you are on and which step failed - The exact error message or symptom - Your Salesforce instance type (`production` or `test`) and Field Nation environment [Contact Field Nation Support](https://support.fieldnation.com) --- ### Workflow URL: /docs/connectors/platforms/salesforce/workflow ## Prerequisites Complete these before starting. Each card links to where you get or confirm the requirement. The connector is installed, credentials saved, and connection test passed. Copied from Configure Field Nation Step 5 and stored securely. System Administrator profile — needed to create Flows and Outbound Messages in Setup. A checkbox or status field on your object that signals when to dispatch to Field Nation. --- ## Architecture Overview The Salesforce workflow consists of two components: ```mermaid graph LR A[Record Created/Updated] --> B{Flow Evaluates
Conditions} B -->|Conditions Met| C[Outbound Message] C --> D[Field Nation Trigger URL] D --> E[Work Order Created] B -->|Conditions Not Met| F[No Action] ``` **Flow**: Monitors records and evaluates conditions **Outbound Message**: Sends record ID to Field Nation **Field Nation**: Fetches full record data and creates work order --- ## Step 1: Create Outbound Message Outbound Messages send record IDs to Field Nation's trigger URL. ### Navigate to Setup Salesforce Setup → Quick Find → "Outbound Messages" ### Create New Message Click "New Outbound Message" ![Outbound Message](./images/outbound_message.webp) ### Select Object Choose the same object you configured in Field Nation (e.g., `Case`) ### Configure Message **Name**: Descriptive name (e.g., "Send to Field Nation") **Unique Name**: Auto-populated (can customize) **Endpoint URL**: Paste Field Nation trigger URL ``` https://api.fieldnation.com/integrations/trigger/{YOUR_CLIENT_TOKEN} ``` ### Select Fields **Important**: Only select the **ID field** Field Nation retrieves all other fields via API using the ID. **Why only ID?** - Reduces message size - Ensures latest data (fetched at processing time) - Prevents stale data if record updates between trigger and processing ### Additional Settings - ☐ **Send Session ID**: Leave **unchecked** (not needed) - ☐ **Protected Component**: Leave **unchecked** (unless in managed package) ### Save Message Click "Save" - Salesforce generates WSDL and endpoint configuration --- ## Step 2: Prepare Trigger Field Create or identify the field that will trigger work order creation. ### Option A: Create Checkbox Field Most common approach - gives users explicit control. ### Navigate to Object Manager Setup → Object Manager → Your Object (e.g., Case) ### Fields & Relationships Click "Fields & Relationships" → "New" ### Select Checkbox Field Type: Checkbox ### Configure Field **Field Label**: "Send to Field Nation" **Field Name**: Auto-populated (e.g., `Send_to_Field_Nation__c`) **Default Value**: Unchecked **Description**: "Check to create Field Nation work order" ![Trigger Field Example](./images/flows7.webp) ### Field-Level Security Grant access to appropriate profiles ### Page Layouts Add field to relevant page layouts so users can see/modify it --- ### Option B: Use Existing Status/Picklist Alternative approach - trigger based on status change. **Example Configurations:** - Case Status = "Dispatch Required" - Priority = "High" AND Status = "Approved" - Record Type = "Field Service" AND Stage = "Scheduled" > [INFO] **Best Practice**: Checkbox provides better control and prevents accidental triggers. Status-based triggering requires careful condition design to prevent unwanted work order creation. --- ## Step 3: Create Record-Triggered Flow Flows provide the automation logic to trigger outbound messages. ### Open Flow Builder Setup → Quick Find → "Flows" → "New Flow" ![Flows Page](./images/flows1.webp) ### Select Flow Type Choose **"Record-Triggered Flow"** ![New Flow](./images/flows2.webp) ![Record-Triggered Flow](./images/flows3.webp) --- ### Configure Trigger Settings ![Flow Trigger Configuration](./images/flows4.webp) #### Object Select the Salesforce object (must match Field Nation configuration and Outbound Message) **Example**: `Case` --- #### Trigger Event Choose when the Flow should run: **A record is created or updated** (Most common) - Creates work order for new records - Updates work order when record changes - Flexible for various scenarios **A record is created** (Initial dispatch only) - Only triggers on new records - Subsequent updates don't trigger - Use when you want one-time creation **A record is updated** (Updates only) - Only triggers on updates to existing records - Won't create work orders for new records **A record is deleted** (Rare) - Triggers when record deleted - Use for cleanup scenarios --- #### Entry Conditions Define exactly when the Flow should run. **Option 1: Simple Condition (AND logic)** Select **"All Conditions Are Met (AND)"** Add condition: - **Field**: `Send to Field Nation` (your trigger checkbox) - **Operator**: `Equals` - **Value**: `True` **Option 2: Complex Conditions (Custom logic)** Select **"Custom Condition Logic (Advanced)"** **Examples:** ``` 1. Send_to_Field_Nation__c = TRUE AND Status = "Approved" 2. (Status = "Dispatch Required" OR Priority = "High") AND RecordType.Name = "Field Service" 3. Send_to_Field_Nation__c = TRUE AND Account.Type = "Customer" AND NOT(ISBLANK(Description)) ``` --- #### Additional Conditions (Optional) Add filters to control when work orders are created: **By Status:** ``` Status = "Approved" Status != "Cancelled" Status IN ("New", "Assigned", "In Progress") ``` **By Record Type:** ``` RecordType.Name = "Field Service" RecordType.DeveloperName = "On_Site_Service" ``` **By Owner/Assignment:** ``` Owner.Department = "Field Operations" Owner.UserRole.Name CONTAINS "Service" ``` **By Field Validation:** ``` NOT(ISBLANK(Account.Name)) NOT(ISBLANK(Priority)) Amount > 0 ``` --- #### Optimize the Flow For Select **"Actions and Related Records"** This enables: - Calling Outbound Messages - Accessing related object data - Better performance for this use case --- #### Click Done Save the trigger configuration --- ### Add Outbound Message Action ![Add Action](./images/flows5.webp) ### Add Action Element Click the **+** icon after the Start element → Select **"Action"** ### Find Outbound Message In the left panel, scroll to **"Outbound Message"** category → Expand it ### Select Your Message Choose the Outbound Message you created earlier ![Select Outbound Message](./images/flows5.5.webp) ### Configure Action **Label**: Descriptive name (e.g., "Send to Field Nation") **API Name**: Auto-populated **Description**: Optional description ### Set Input Values **No configuration needed** - Outbound Message automatically uses the triggering record ### Click Done The action is added to your Flow --- ### Complete Flow Your Flow should now look like this: ![Complete Flow](./images/flows6.webp) 1. **Start**: Trigger conditions evaluated 2. **Action**: Outbound Message sent 3. **End**: Flow completes --- ## Step 4: Save & Activate Flow ### Save Flow Click **"Save"** button in toolbar **Flow Label**: Descriptive name (e.g., "Create Field Nation Work Order") **Flow API Name**: Auto-populated **Description**: Purpose of the Flow (e.g., "Sends Cases to Field Nation when Send to FN checkbox is checked") ### Activate Flow Click **"Activate"** button **Before Activation:** - Flow exists but doesn't run - Good for testing configuration **After Activation:** - Flow actively monitors records - Triggers when conditions met - Can be deactivated later if needed ### Verify Status Return to Flows page → Verify status shows **"Active"** ![Active Flow](./images/flows8.webp) > **Important**: Flow must be **Active** to process records. Saved but inactive Flows won't trigger. --- ## Step 5: Test the Integration Thoroughly test before production use. ### Create Test Record ### Navigate to Your Object Go to Cases (or your configured object) ### Create New Record Click "New" → Fill in required fields **Test Data:** - Subject/Title: "Test FN Integration" - Description: "Testing Salesforce to Field Nation sync" - Priority: Select appropriate value - Any other required/mapped fields ### Check Trigger Field ✅ Check **"Send to Field Nation"** (or set status to trigger value) ### Save Record Click "Save" Flow triggers immediately on save --- ### Verify in Field Nation ### Log into Field Nation Access your Field Nation account (Sandbox or Production - match your configuration) ### Navigate to Work Orders Dashboard → Work Orders → All Work Orders ### Find Test Work Order Look for recently created work order with title from Salesforce ### Verify Field Mappings Check that data populated correctly: - Title matches Case Subject - Description matches Case Description - Location/Company from Salesforce Account - Custom fields populated - All required fields present ### Check Correlation ID Work order should have reference to Salesforce record ID (for bidirectional sync) --- ### Workflow setup complete --- ## Troubleshooting ### Work Order Not Created **Check:** - ☐ Flow status = "Active" - ☐ Trigger field checked/set correctly - ☐ Record meets ALL entry conditions - ☐ No validation rules blocking save **Debug:** - Setup → Debug Logs → Enable debug for your user - Repeat test record creation - Review debug log for Flow execution **Check:** - ☐ Outbound Message endpoint URL correct - ☐ Flow action configured correctly - ☐ No network/firewall blocking outbound HTTPS **View Queue:** - Setup → Outbound Messages → "View Queue" - Check for pending or failed messages - Click message ID for error details **Test Endpoint:** ```bash curl -X POST https://api.fieldnation.com/integrations/trigger/{YOUR_TOKEN} \ -H "Content-Type: text/xml" \ -d '' ``` **Check:** - ☐ Field Nation configuration complete - ☐ Credentials valid (test connection) - ☐ Trigger URL matches Outbound Message endpoint - ☐ Object name matches Salesforce object **Review Logs:** - Field Nation → Integrations → Logs - Filter by date/time of test - Look for incoming webhook and processing status **Check:** - ☐ Fields refreshed in Field Nation - ☐ Required FN fields all mapped - ☐ Data types compatible - ☐ Picklist values mapped correctly (Array Map) **Test:** - Create simple record with minimal data - Verify basic fields sync first - Add complex mappings incrementally --- ### Common Errors **"INVALID_SESSION_ID"** - Field Nation credentials expired - Regenerate security token - Update Field Nation configuration **"INSUFFICIENT_ACCESS"** - Salesforce API user lacks permissions - Grant "API Enabled" permission - Verify object and field permissions **"REQUIRED_FIELD_MISSING"** - Missing required Field Nation work order fields - Add field mappings or default values - Check Field Nation logs for specific field name **Outbound Message Timeout** - Field Nation processing too slow - Contact Field Nation Support - Usually resolves within seconds --- ## Advanced Configurations ### Multiple Outbound Messages Create separate Flows for different scenarios: **Example:** - Flow 1: Urgent Cases → Priority handling - Flow 2: Standard Cases → Normal processing - Flow 3: Updates Only → Sync status changes **Benefits:** - Different field mappings per scenario - Conditional routing - Better monitoring/debugging --- ### Prevent Duplicate Sends Add logic to prevent re-triggering: ### Add Decision Element After Start → Add Decision element before Outbound Message ### Check Sync Status **Decision Criteria:** - Custom field `FN_Sync_Status__c` = null OR "Not Sent" ### Route Logic - **Met Criteria**: Send Outbound Message - **Not Met**: End (don't send) ### Update After Send Add another action after Outbound Message: - Update Record: Set `FN_Sync_Status__c` = "Sent" --- ### Schedule-Based Triggering Use Scheduled Flow instead of Record-Triggered for batch processing: **When to Use:** - High volume (avoid API limits) - Off-peak processing - Batch creation vs real-time **Configuration:** - Flow Type: "Scheduled Flow" - Frequency: Daily, Weekly, etc. - Query: Get Records where `Send_to_FN__c = TRUE AND FN_Sync_Status__c = null` - Loop through records → Send Outbound Message for each --- ## Production Deployment ### Pre-Launch Checklist - ☐ Sandbox testing complete and successful - ☐ All field mappings validated - ☐ Error handling tested - ☐ User training completed - ☐ Monitoring/alerting configured - ☐ Rollback plan documented ### Launch Steps ### Deploy to Production Use Change Sets or Salesforce DX to deploy: - Outbound Message - Custom fields (if created) - Flow (initially as Inactive) - Page layout changes ### Update Field Nation Configuration Switch Field Nation from Sandbox to Production: - Update credentials (production Salesforce user) - Update instance type to "production" - Test connection - Copy new trigger URL ### Update Outbound Message Update endpoint URL with production trigger URL ### Activate Flow After successful testing, activate the Flow in production ### Monitor Closely Watch first few syncs for any issues --- ## Maintenance ### Regular Tasks **Weekly:** - Review Outbound Message queue for failures - Check Field Nation integration logs - Monitor API usage in Salesforce **Monthly:** - Review and update field mappings - Test integration with sample records - Update documentation for any changes **Quarterly:** - Rotate security token (update Field Nation) - Review and optimize Flow conditions - Audit integration usage and performance --- --- ### Configuration URL: /docs/connectors/platforms/servicenow/configuration ## Before you begin Complete the [prerequisites](/docs/connectors/platforms/servicenow/overview#prerequisites) on the Overview page first. You should have: - Field Service Management plugin active (for `wm_task`) - A dedicated service account with the required roles - An OAuth 2.0 application registered in ServiceNow --- ## Step 1: Access Integration Broker Open the Integration Broker: - **Sandbox**: [ui-sandbox.fndev.net/integrations](https://ui-sandbox.fndev.net/integrations) - **Production**: [app.fieldnation.com/integrations](https://app.fieldnation.com/integrations) Select **ServiceNow** from the list of available connectors. --- ## Step 2: Basic settings ![ServiceNow Integration Broker basic settings panel](./images/servicenow_settings.webp) ### Instance name (required) Your ServiceNow instance name — the subdomain portion of your instance URL. For example, if your instance is `https://dev12345.service-now.com`, enter: ``` dev12345 ``` > Enter only the instance name (e.g., `dev12345`), **not** the full URL. The connector constructs the full URL automatically. Entering a full URL like `https://dev12345.service-now.com` will cause a connection failure. ### Table name (required) The ServiceNow table API name. For this guide: ``` wm_task ``` > [INFO] The connector defaults to `incident`. To use `wm_task` (Work Order Task), change the Object Name field and ensure the Field Service Management plugin is active. Use the table API name (`wm_task`), not the display label. --- ## Step 3: Authentication ### OAuth 2.0 (recommended) The connector uses OAuth 2.0 with the **Resource Owner Password Credentials (ROPC)** grant type. This requires the service account and OAuth application you created during prerequisites. In Integration Broker, select **OAuth 2.0** and enter: | Field | Value | | ------------- | ------------------------------------------------- | | Client ID | From ServiceNow Application Registry | | Client Secret | From ServiceNow Application Registry | | Username | Service account (e.g., `integration.fieldnation`) | | Password | Service account password | > [INFO] The connector automatically constructs the token URL from your instance name and always uses the OAuth 2.0 password grant. There is no separate Token URL or Grant Type field — these are handled internally. OAuth 2.0 is the only supported authentication method. --- ## Step 4: Test connection Click **Test Connection**. The Integration Broker will: 1. Authenticate with your ServiceNow instance 2. Query the target table metadata 3. Verify the service account has the required permissions **Success**: Green confirmation — connection established, table accessible. **Failure**: Review the error and check the troubleshooting section below. ### Troubleshooting connection errors **Cause**: Invalid credentials. **Fix:** 1. Verify Client ID and Secret match the ServiceNow Application Registry record 2. Confirm the OAuth application is active (not disabled) 3. Ensure the instance name is correct (the connector builds the token URL automatically as `https://.service-now.com/oauth_token.do`) 4. Verify the service account password has not expired **Cause**: Service account lacks required roles or table web service access is disabled. **Fix:** Check the following based on your enabled features: - `wm_dispatcher` — always required (base role for `wm_task` read/write) - `wm_admin` — only if Outbound Create is enabled (replaces `wm_dispatcher`) - `itil` — only if attachment sync is enabled - `personalize_dictionary` — only if Template Bypass is OFF - `snc_platform_rest_api_access` — only if the Table API ACL is activated on your instance Also verify that **"Allow access to this table via web services"** is enabled for each table the connector accesses (e.g., `wm_task`, `sys_attachment`). See the [prerequisites](/docs/connectors/platforms/servicenow/overview#prerequisites) for the full permissions matrix. **Cause**: Table name incorrect or does not exist. **Fix:** 1. Confirm you entered `wm_task` (not "Work Order Task") 2. Verify the Field Service Management plugin is active 3. Check the table exists: type `wm_task.list` in the ServiceNow filter navigator --- ## Step 5: Get Trigger URL Once the connection test passes, Field Nation generates your unique **Trigger URL**. It appears in the **Trigger Information** section of the settings page. > Always copy the Trigger URL directly from the Integration Broker UI. Do not manually construct it — the base path varies by environment (production vs. sandbox). **Copy this URL.** You will need it when configuring the REST Message in ServiceNow (covered in the [Workflow Setup](/docs/connectors/platforms/servicenow/workflow) page). > The client token authenticates incoming requests from ServiceNow. Treat it like a secret — do not share it or commit it to source control. Your reference sheet keeps it in your browser so it carries over to the Workflow page. --- ## Step 6: Refresh fields Click **Refresh Fields** to discover available ServiceNow fields for mapping. The broker queries your instance's metadata and populates the mapping dropdowns with: - Standard fields (`short_description`, `description`, `state`, `priority`) - Custom fields (`u_custom_field`) - Reference fields (`assignment_group.name`, `location.city`) - Choice values (for state, priority, etc.) --- ## Step 7: Template bypass (optional) By default, the connector reads the `sys_db_object` and `sys_dictionary` tables to discover available fields. If your organization's security policies restrict ACL access to these tables, standard field discovery will fail. Configure **Template Bypass** to discover fields from a sample record instead. This is also useful for mapping deeply nested reference lookups that the dictionary method may not resolve. ### In ServiceNow ### Identify a "golden" record Find an existing Work Order Task (`wm_task`) that has a Subproject and Project Manager already assigned. ### Verify field data is populated Open the record and confirm the fields you want to map are **not blank**. ServiceNow excludes empty fields from API responses — if a field is blank on the template, it will be invisible to Field Nation. ### Copy the identifier Copy either the **Record Number** (e.g., `WOT0010014`) or the 32-character **Sys ID**. ### In Field Nation ### Toggle on bypass Navigate to the Integration **Settings** tab. Set the **Restrict SYS Dictionary ACL** switch to **ON**. ### Enter the template ID Paste the Record Number or Sys ID into the **Template Object ID** field. ### Save and refresh Click **Save**, then click **Refresh Fields**. Inherited fields will now appear in the mapping dropdowns. > **Drawback**: New custom fields added to your ServiceNow table later will not auto-discover. You must manually populate the field on the template record and click "Refresh Fields" again. If the template record is deleted, field discovery breaks. --- ## Step 8: Configure field mappings Map data between ServiceNow and Field Nation. ### Inbound mappings (ServiceNow → Field Nation) These control how ServiceNow data populates your Field Nation work order: | ServiceNow Field | Field Nation Field | Action | | ------------------- | --------------------- | ------------ | | `short_description` | Title | Sync | | `description` | Description | Sync | | `location.name` | Location Company Name | Sync | | `priority` | Priority | Array Map | | `state` | Status | Array Map | | `caller_id.email` | Contact Email | Sync | | `work_start` | Schedule Start | Date Convert | ### Outbound mappings (Field Nation → ServiceNow) These control how Field Nation updates flow back to ServiceNow: | Field Nation Field | ServiceNow Field | Action | | ------------------ | ------------------ | ------------ | | Status | `state` | Array Map | | Assignee Name | `assigned_to.name` | Sync | | Completion Notes | `work_notes` | Sync | | Completion Date | `resolved_at` | Date Convert | ### How to add a mapping 1. In the Integration Broker, click **Add Mapping** under the Inbound or Outbound section 2. Select the **ServiceNow field** from the left dropdown — populated after Refresh Fields 3. Select the **Field Nation field** from the right dropdown 4. Choose the **Action** (`Sync`, `Array Map`, or `Date Convert`) 5. For `Array Map`, click **Edit Values** and add one row per value pair 6. Click **Save** to persist the mapping ### State/status array map Map ServiceNow numeric state values to Field Nation status IDs: ```jsonnet { "source": "state", "target": "status_id", "action": "array_map", "mappings": [ { "compare": "1", "value": "1" }, // Open → Draft { "compare": "3", "value": "1" }, // Pending Dispatch → Draft { "compare": "2", "value": "2" }, // In Progress → Assigned { "compare": "4", "value": "3" }, // Closed Complete → Work Done { "compare": "5", "value": "3" } // Closed Incomplete → Work Done ], "default": "1" } ``` **ServiceNow `wm_task` state values:** | Value | State | | ----- | ----------------- | | 1 | Open | | 2 | In Progress | | 3 | Pending Dispatch | | 4 | Closed Complete | | 5 | Closed Incomplete | | 7 | Cancelled | [Complete field mapping guide →](/docs/connectors/concepts/field-mappings) --- ## Step 9: Save configuration ### Review all settings Verify: instance URL, table name, authentication, field mappings (inbound and outbound). ### Click Save Persist your configuration. ### Copy the Trigger URL You will use this in the next step — configuring the ServiceNow REST Message. --- ## Enable optional features The following features are disabled by default. Enable them in the Integration **Settings** tab after saving your base configuration. | Feature | What it does | ServiceNow role required | | --- | --- | --- | | **Outbound Create** | Allows Field Nation to create new `wm_task` records directly via the REST Table API | `wm_admin` (replaces `wm_dispatcher`) | | **Attachment sync** | Syncs file attachments between Field Nation work orders and ServiceNow records | `itil` | | **Message and notes sync** | Syncs Field Nation notes to `work_notes` and ServiceNow `work_notes` back to Field Nation | No additional role required | > [INFO] Each feature requires the corresponding ServiceNow role on your service account. Re-run **Test Connection** after enabling a feature to verify the service account has the necessary permissions. --- ## Verify the integration After saving, confirm the connector is working end-to-end before proceeding to workflow setup. ### Run an inbound test 1. In the Integration Broker, navigate to the **Event History** tab 2. Use the **Test Trigger** button (if available) or proceed to [Workflow Setup](/docs/connectors/platforms/servicenow/workflow) and create a test task 3. Trigger the Business Rule by changing a task to Pending Dispatch 4. Return to Event History and confirm a new event appears ### Interpreting event history | Event status | Meaning | Action | | --- | --- | --- | | `received` | Trigger URL was called — broker received the `sys_id` | Normal — processing | | `fetched` | Broker successfully read the full record from ServiceNow | Normal — creating work order | | `created` | Work order created in Field Nation | Success | | `mapping_error` | A field mapping failed — value incompatible with target field | Review inbound mappings | | `auth_error` | ServiceNow rejected the broker's read request | Verify service account credentials | | `not_found` | `sys_id` sent in trigger does not match a readable record | Check service account table ACLs | --- ### Your reference sheet --- **Next:** [Workflow Setup →](/docs/connectors/platforms/servicenow/workflow) --- ### Overview URL: /docs/connectors/platforms/servicenow/overview The ServiceNow connector automatically creates Field Nation work orders when a Work Order Task (`wm_task`) reaches a dispatch-ready state. A Business Rule in ServiceNow detects the state change and sends a notification to Field Nation, which fetches the full record and creates a work order — no manual re-entry required. > [INFO] This connector also supports the `incident` table. The setup steps are identical — only the table name and filter conditions differ. OAuth 2.0 Work Order Task (`wm_task`) Business Rules + REST Messages Bidirectional (status, notes, attachments, messages) --- ## How it works ```mermaid sequenceDiagram participant SN as ServiceNow participant BR as Business Rule participant RM as REST Message participant Broker as Integration Broker participant FN as Field Nation SN->>BR: Task state changes to Pending Dispatch BR->>RM: Execute REST Message RM->>Broker: POST trigger URL (sys_id) Broker->>SN: GET full record via REST API SN-->>Broker: Record data + related fields Broker->>Broker: Apply field mappings Broker->>FN: Create work order FN-->>Broker: Work order ID ``` ### Business Rule fires A Work Order Task changes to **Pending Dispatch**. Your Business Rule detects this and executes a REST Message. ### REST Message notifies Field Nation The REST Message sends the record's `sys_id` to your unique Field Nation trigger URL. ### Integration Broker fetches record data Field Nation authenticates back to ServiceNow and retrieves the full record, including related table fields. ### Work order is created Field mappings transform ServiceNow data into a Field Nation work order. A correlation ID links the two records for bidirectional sync. --- ## Configuration stages ![ServiceNow connector setup journey — four stages from Prerequisites through Test & Validate](./images/servicenow-setup-journey.webp) --- ## Use cases Dispatch Field Nation work orders automatically when a `wm_task` record reaches a dispatch-ready state — no manual hand-off required. ```mermaid flowchart LR A[Task created in ServiceNow] --> B[State changes to Pending Dispatch] B --> C[Business Rule fires] C --> D[REST Message sends sys_id] D --> E[Integration Broker fetches record] E --> F[Work order created in Field Nation] ``` Filter which tasks reach Field Nation using Business Rule conditions — dispatch only tasks that meet criteria such as category, location, or assignment group. ```mermaid flowchart LR A[Task state → Pending Dispatch] --> B{Filter condition met?} B -->|Yes| C[Business Rule fires] B -->|No| D[No action] C --> E[Field Nation work order created] ``` When a Field Nation work order status changes — assigned, work done, or cancelled — the Integration Broker sends an outbound update back to the originating `wm_task` record via the REST Table API. ```mermaid flowchart LR A[Work order status changes in Field Nation] --> B[Integration Broker] B --> C[Outbound mapping applied] C --> D[PATCH wm_task via REST API] D --> E[ServiceNow record updated] ``` Notes added in Field Nation sync to `work_notes` in ServiceNow, and attachments added to either side sync to the other — keeping both records aligned throughout the work order lifecycle. ```mermaid flowchart LR A[Note added in Field Nation] --> B[Integration Broker] B --> C[Maps to work_notes] C --> D[wm_task updated in ServiceNow] E[Attachment in either system] --> B ``` --- ## The setup journey The full integration takes approximately **60 minutes** from start to first successful work order. | Stage | Who completes it | Estimated time | | --- | --- | --- | | Stage 1 — ServiceNow prerequisites | ServiceNow administrator | ~20 min | | Stage 2 — Configure Field Nation connector | Field Nation administrator | ~15 min | | Stage 3 — Workflow setup in ServiceNow | ServiceNow administrator | ~15 min | | Stage 4 — Test and validate | Both administrators | ~10 min | > [INFO] Stages 1 and 3 require ServiceNow admin access. You can hand off the [Prerequisites](#prerequisites) section and the [Workflow Setup](/docs/connectors/platforms/servicenow/workflow) page to your ServiceNow administrator to complete independently. --- ## Before you begin Complete the checks below before starting [Configuration](/docs/connectors/platforms/servicenow/configuration). The ServiceNow prerequisites (Stage 1) must be completed by a ServiceNow administrator and can be handed off to them independently. --- ## Prerequisites ### What you'll need from ServiceNow Complete these steps in ServiceNow before configuring the connector in Field Nation. > An active ServiceNow instance with admin access is required. #### 1. Install the Field Service Management plugin The `wm_task` table requires this plugin to be active. 1. Log in as an administrator 2. Navigate to **System Definition → Plugins** 3. Search for **Field Service Management** or `com.snc.work_management` 4. Click the plugin and select **Install** (or **Activate**) 5. Wait for activation to complete, then reload #### 2. Create a dedicated service account Create a non-personal, non-interactive integration user. The connector authenticates as this user. 1. Navigate to **User Administration → Users** and click **New** 2. Set a standard name (e.g., User ID: `integration.fieldnation`) 3. Prevent interactive logins: - **Newer versions**: Check **Internal Integration User** or set Identity type to **API Only** - **Older versions**: Check **Web service access only** 4. Right-click the header and click **Save** 5. Open the **Roles** related list and assign: | Role | Purpose | When required | | ------------------------------ | ------------------------------------------------------------------------- | ----------------------------------------------------------------------- | | `wm_dispatcher` | Read/write `wm_task` records (unscoped) | Always (base role) | | `wm_admin` | Full CRUD on `wm_task` — includes record creation | Only if Outbound Create is enabled (replaces `wm_dispatcher`) | | `itil` | Read/write attachments via `sys_attachment` | Only if attachment sync is enabled | | `personalize_dictionary` | Field discovery via schema tables | Only if Template Bypass is OFF | | `snc_platform_rest_api_access` | Allows Field Nation to send bidirectional updates back via REST Table API | Only if the Table API ACL is activated on the instance (off by default) | > Keep this service account on the **global** domain. If a release migration (for example, to the **Australia** release) moves it into a sub-domain, the integration can lose required permissions and updates may fail (often showing up as auth or business-rule errors). See [Australia Release Migration](/docs/connectors/platforms/servicenow/guides/australia-release-upgrade) before and after migrating to the Australia release. #### 3. Register an OAuth 2.0 application 1. Navigate to **System OAuth → Application Registry** 2. Click **New** → **Create an OAuth API endpoint for external clients** 3. Name it (e.g., "Field Nation Integration") 4. Leave **Redirect URL** blank 5. Save and copy the generated **Client ID** and **Client Secret** #### 4. Verify table access controls (ACLs) The roles above satisfy standard ACLs. In a customized environment, confirm these tables are accessible: | Table | Access | Purpose | | ------------------ | ------------ | ---------------------------------------------- | | `wm_task` | Read + Write | Bidirectional work order sync | | `sys_attachment` | Read + Write | Upload/download attachments | | `sys_db_object` | Read | Field discovery (unless using Template Bypass) | | `sys_dictionary` | Read | Field discovery (unless using Template Bypass) | | `sys_glide_object` | Read | Field type resolution during discovery | > Each table the connector accesses must have **"Allow access to this table via web services"** enabled in ServiceNow Application Access settings. This is a per-table toggle — if disabled, API calls return errors regardless of role assignments. ### What you'll need from Field Nation - A Field Nation account with access to the **Integration Broker** (admin role required) - Your ServiceNow OAuth credentials ready: Client ID, Client Secret, service account username and password ### Your reference sheet Have these details ready before opening the Field Nation Integration settings. Fill them in as you go — your entries are saved in your browser and carry across the Configuration and Workflow pages. --- ## Terminology | Term | Definition | | --- | --- | | `wm_task` | ServiceNow table for Work Order Tasks — the primary source record this connector reads from | | Business Rule | A server-side script in ServiceNow that fires when a table row meets defined conditions | | REST Message | A ServiceNow outbound HTTP call definition, configured with endpoint, headers, and body | | Integration Broker | Field Nation middleware that receives inbound webhook triggers and orchestrates work order creation | | Trigger URL | The unique HTTPS endpoint Field Nation provides for receiving webhook calls from ServiceNow | | `sys_id` | ServiceNow's 32-character unique identifier for any record — sent in the webhook payload | | Field mappings | Rules in the Integration Broker that transform ServiceNow field values into Field Nation work order fields | | Template Bypass | Alternative field discovery that reads from a sample record instead of schema metadata tables | | Correlation ID | An identifier Field Nation attaches to a work order to link it back to the originating ServiceNow record | | ROPC | Resource Owner Password Credentials — the OAuth 2.0 grant type this connector uses for authentication | --- ## Key concepts ### How the trigger works The integration is pull-based at its core: ServiceNow **pushes a notification** (the `sys_id`), and Field Nation **pulls the full record**. This means: - The REST Message only needs to send the record identifier — not the full payload - Field Nation reads the record at the time of trigger, so field values are always current - The service account's permissions determine which fields are visible to Field Nation ### Correlation ID When Field Nation creates a work order from a ServiceNow trigger, it stores a correlation ID linking the two records. This ID is the mechanism for all bidirectional sync — status updates, notes, and attachments flow back to ServiceNow using this link. ### OAuth 2.0 authentication The connector uses the Resource Owner Password Credentials (ROPC) grant. Field Nation stores the service account credentials and exchanges them for an access token against your instance's token endpoint (`https://.service-now.com/oauth_token.do`) on each authenticated request. ### Bidirectional sync Once a work order is created, Field Nation sends outbound updates back to the `wm_task` record when work order status changes. The outbound field mappings in the Integration Broker control exactly which Field Nation fields update which ServiceNow fields. --- ## Troubleshooting **Check in ServiceNow:** - Business Rule is **Active** - Filter condition uses `changes to` (not `is`) - **Insert** and **Update** are both checked on the When to run tab - Table is `wm_task` **Check in Field Nation:** - Configuration is saved (not in draft state) - Test Connection shows a green confirmation - Required field mappings are configured **Debug:** Check **System Logs → All** in ServiceNow for `Field Nation Trigger queued` messages. If the message appears, the Business Rule fired and the issue is downstream in Field Nation. **Common causes:** - Client ID or Secret copied incorrectly - OAuth application is disabled in ServiceNow - Service account password has expired - Instance name includes `https://` or `.service-now.com` — enter only the subdomain Re-run **Test Connection** in the Integration Broker after correcting credentials. **Cause:** Field discovery is failing or returning incomplete results. **Fix:** 1. Verify `sys_db_object` and `sys_dictionary` tables are accessible to the service account 2. If those tables are ACL-restricted, enable [Template Bypass](/docs/connectors/platforms/servicenow/configuration#step-7-template-bypass-optional) using a populated sample record 3. Ensure the sample record has all fields you want to map populated with non-blank values **Cause:** Business Rule firing more than once for the same state transition. **Fix:** 1. Confirm the filter uses `changes to` not `is` 2. Check for other Business Rules on `wm_task` that may also trigger the integration 3. Verify that Insert and Update are not both catching the same state transition --- ## Need help? Connect Field Nation to your ServiceNow instance, authenticate, and map fields. Create the REST Message and Business Rule in ServiceNow to trigger work order creation. --- ### Workflow URL: /docs/connectors/platforms/servicenow/workflow ## Prerequisites Complete these before starting. Each card links to where you get or confirm the requirement. The connector is authenticated, fields are discovered, and configuration is saved. Copied from Configuration Step 5 and stored in your reference sheet. Admin or developer access needed to create REST Messages and Business Rules. --- ## What you'll build Two components in ServiceNow that work together: 1. **REST Message** — Defines the HTTP request that notifies Field Nation when a task is ready 2. **Business Rule** — Monitors the `wm_task` table and fires the REST Message when a task reaches Pending Dispatch ```mermaid graph LR A[Task state → Pending Dispatch] --> B[Business Rule fires] B --> C[REST Message sends sys_id] C --> D[Field Nation creates work order] ``` > [INFO] You need the **Trigger URL** from the [Configuration](/docs/connectors/platforms/servicenow/configuration#step-5-get-trigger-url) page before starting. If you haven't completed configuration, do that first. --- ## Step 1: Create the REST Message This defines the HTTP request ServiceNow will send to Field Nation. ### Navigate to REST Messages Go to **System Web Services → Outbound → REST Message** and click **New**. ![REST Message list in ServiceNow — click New to create](./images/servicenow_workflow_1.webp) ### Set the name and endpoint **Name**: `Field Nation Webhook` **Endpoint**: Paste only the **base URL** of your Trigger URL — everything before the `?`. For example: ``` https://micro.fieldnation.com/v1/broker/inbound ``` > Copy the base URL from your Trigger URL in the Integration Broker. Do not type it manually — the path differs between production and sandbox environments. The full Trigger URL contains a `client_token` — treat it as sensitive. Your reference sheet keeps it in your browser, so it's available here from Configuration. Right-click the top header and click **Save**. ### Open the Default POST method Scroll down to the **HTTP Methods** tab and click **Default POST**. > [INFO] If you only see Default GET, change it to POST. ### Add query parameters Scroll to the **HTTP Query Parameters** section. Add two rows: | Name | Value | | -------------- | -------------------------------------- | | `client_token` | _(Paste your token from Field Nation)_ | | `external_id` | `${sys_id}` | ![HTTP Query Parameters section with client_token and external_id rows added](./images/servicenow_workflow_2.webp) > Do not hardcode `client_token` into the Endpoint URL field. ServiceNow may strip query strings pasted directly into URL fields. Add it as a query parameter instead. > Use the exact variable format `${sys_id}` for `external_id`. ServiceNow dynamically injects the triggering record's unique ID at runtime. > [INFO] The `external_id` query parameter name and `client_token` are defined by the Integration Broker's inbound endpoint. The Integration Broker UI may display a note saying "We will look in the POST body for the `id`" — the broker accepts both approaches. Using `external_id` as a query parameter (as shown above) is the recommended method for ServiceNow Business Rule triggers. ### Add the Content-Type header Scroll to the **HTTP Headers** section and add: | Name | Value | | -------------- | ------------------ | | `Content-Type` | `application/json` | ### Set the request body Click the **HTTP Request** tab. In the **Content** box, paste: ```json { "sys_id": "${sys_id}", "table": "${table}", "timestamp": "${timestamp}" } ``` ### Save Click **Update** to save the HTTP method. --- ### Test the REST Message Before creating the Business Rule, verify the REST Message works: ### Open Default POST In the HTTP Methods tab, click **Default POST**. ### Set test variables In the **Variable Substitutions** section, enter: | Variable | Test Value | | ----------- | --------------- | | `sys_id` | `test_12345` | | `table` | `wm_task` | | `timestamp` | `1714300000000` | ### Click Test Scroll to the bottom and click **Test**. ### Verify the response **Success**: HTTP 200 response. The trigger was received by Field Nation (no work order is created from a test `sys_id`). **Failure**: Check the endpoint URL and `client_token` value. --- ## Step 2: Create the Business Rule This script monitors the `wm_task` table and triggers the REST Message when your conditions are met. ### Navigate to Business Rules Go to **System Definition → Business Rules** and click **New**. ### Set basic fields | Field | Value | | ------------ | ----------------------------------- | | **Name** | `Push to Field Nation` | | **Table** | `wm_task` | | **Active** | Checked | | **Advanced** | Checked (reveals the script editor) | ![Business Rule basic fields — Name, Table, Active, and Advanced checked](./images/servicenow_workflow_4.webp) ### Configure "When to run" Click the **When to run** tab: | Setting | Value | | ---------- | ------- | | **When** | `after` | | **Insert** | Checked | | **Update** | Checked | ### Set filter conditions Configure the condition using three dropdowns: | Dropdown | Value | | ------------ | ------------------ | | **Field** | `State` | | **Operator** | `changes to` | | **Value** | `Pending Dispatch` | > Use **changes to**, not **is**. The `changes to` operator fires only on the state transition. Using `is` causes duplicate API calls every time the record is saved while already in that state. ### Add the script Click the **Advanced** tab. Paste the following into the **Script** field: ```javascript (function executeRule(current, previous /*null when async*/) { try { var r = new sn_ws.RESTMessageV2("Field Nation Webhook", "Default POST"); r.setStringParameterNoEscape("sys_id", current.sys_id.toString()); r.setStringParameterNoEscape("table", current.getTableName()); r.setStringParameterNoEscape( "timestamp", new GlideDateTime().getNumericValue(), ); r.executeAsync(); gs.info( "Field Nation Trigger queued for " + current.getTableName() + ":" + current.sys_id, ); } catch (ex) { gs.error("Field Nation Trigger Error: " + ex.message); } })(current, previous); ``` ![Business Rule Advanced tab showing the trigger script](./images/servicenow_workflow_5.webp) ### Save the Business Rule Click **Submit**. --- ## Step 3: Create your first work order With the REST Message and Business Rule in place, trigger the integration by creating a task that meets your filter conditions. ### Navigate to Work Order Tasks In the left navigation bar, go to **Work Order → Tasks** (or type `wm_task.list` in the filter navigator and press Enter). ### Create a new task Click **New** and fill in the required fields: - Short Description - Description - Location > [INFO] If you configured [Template Bypass](/docs/connectors/platforms/servicenow/configuration#step-7-template-bypass-optional), populate the same fields that exist on your template record. ### Trigger the Business Rule Change the **State** dropdown to **Pending Dispatch**. ### Save the record Right-click the top header and click **Save** (or click **Update**). The Business Rule fires, the REST Message sends the `sys_id` to Field Nation, and a work order is created. ### Verify in Field Nation 1. Log into Field Nation 2. Navigate to **Work Orders** 3. Find the newly created work order 4. Verify the field values match your ServiceNow record 5. Check the **Correlation ID** field on the work order — it should contain the ServiceNow `sys_id`, confirming the link is established for bidirectional sync --- ## Troubleshooting **Check:** - Business Rule is **Active** - Record meets the filter condition (State *changes to* Pending Dispatch) - **Insert** and **Update** are checked on the "When to run" tab - Table is set to `wm_task` **Debug:** Add a log line at the top of the script: ```javascript gs.info("Field Nation BR: Triggered for " + current.sys_id); ``` Then check **System Logs → All** for the message. **Common errors:** | HTTP Code | Cause | Fix | |---|---|---| | 401 | Invalid `client_token` | Re-copy the token from Field Nation Integration Broker | | 404 | Wrong trigger URL | Verify the endpoint matches your Trigger URL (base only, no query params in URL field) | | 500 | Field Nation processing error | Check Integration Broker logs in Field Nation | **Check:** - Field Nation configuration is saved (not draft) - Credentials are valid (re-test connection) - Required Field Nation fields are mapped - The `sys_id` sent matches a real record that the service account can access **Debug:** Check Integration Broker logs in Field Nation for incoming webhook records and mapping errors. **Cause:** Business Rule firing multiple times for the same record. **Fix:** 1. Confirm you are using `changes to` (not `is`) in the filter condition 2. Check for other Business Rules on `wm_task` that may also trigger the REST Message 3. Verify you don't have both Insert and Update triggering for the same state transition --- ## Advanced configurations ### Scope the Business Rule to specific assignment groups Add a second condition to the Business Rule filter to limit dispatch to tasks assigned to a specific group: | Dropdown | Value | | ------------ | ------------------------ | | **Field** | `Assignment Group` | | **Operator** | `is` | | **Value** | _(your target group name)_ | Use **AND** logic between conditions. The Business Rule fires only when both conditions are met. ### Use a custom trigger state The filter condition does not need to be `Pending Dispatch`. You can use any `wm_task` state value — or a custom field — as the trigger. Update the filter condition dropdown values and adjust your inbound field mappings accordingly. ### Disable the Business Rule in non-production instances If you have cloned your ServiceNow instance for development or testing, set the Business Rule to **Inactive** in non-production clones to prevent test data from reaching the Field Nation production environment. Use the sandbox Trigger URL for non-production instances. --- ### Your reference sheet --- --- ### Configuration URL: /docs/connectors/platforms/smartsheet/configuration Complete the steps below in order. **Steps 1–3 are completed inside Smartsheet** and take approximately 5 minutes. Once credentials are ready, complete Steps 4–8 inside Field Nation. **Estimated setup time:** 15–25 minutes total. > [INFO] Before starting, confirm you have Admin or Editor access to the Smartsheet sheet you want to integrate. See [Before you begin](/docs/connectors/platforms/smartsheet/overview#before-you-begin) on the Overview page. --- ## Step 1: Generate a Smartsheet API Access Token The connector authenticates using a personal API Access Token. This token has the same permissions as the user who generated it. ### Log into Smartsheet Use an admin account or a dedicated service account. We recommend a shared service account (e.g., `integrations@yourcompany.com`) so the integration doesn't break if a personal account is deactivated. ### Navigate to API settings Click your **profile icon** (bottom-left) → **Apps & Integrations** → **API Access** ### Generate a new access token Click **Generate new access token**. - **Token name:** `Field Nation Integration` - Copy the token immediately — it is shown only once. ![Smartsheet API Access Token generation dialog with token value redacted](./images/smartsheet-token-generation.webp) > **Store the token securely.** If you lose it, you must revoke and regenerate. The integration will stop working until the new token is entered in Field Nation. --- ## Step 2: Get your Template Sheet ID The **Template Sheet ID** tells the connector which sheet structure to use for column discovery (Refresh Fields). This should be the sheet containing all the columns you want to map. ### Open your sheet in Smartsheet Navigate to the sheet you plan to integrate with Field Nation. ### Find the Sheet ID **Method 1 — Sheet Properties:** File → Properties → copy the **Sheet ID** ![Smartsheet Sheet Properties dialog showing Sheet ID](./images/smartsheet-sheet-properties.webp) **Method 2 — Browser URL:** ``` https://app.smartsheet.com/sheets/4583173393803140 ``` The numeric segment in the URL is your Sheet ID. Example Sheet ID: `4583173393803140` --- ## Step 3: Get your Workspace ID (optional) The Workspace ID is only required if you want the connector to create temporary sheets within a specific workspace during field refresh. **Most customers can skip this step.** ### Navigate to Workspaces Click the **Browse** (folder) icon in the left sidebar → **Workspaces** ### Get the Workspace ID Right-click on your workspace → **Properties** → copy the **Workspace ID** ![Smartsheet Workspace Properties dialog showing Workspace ID](./images/smartsheet-workspace-properties.webp) Example Workspace ID: `7523453512247172` > [INFO] If you leave Workspace ID blank, the connector creates temporary sheets at the account root level during field refresh. They are deleted immediately after — you may briefly see them in your Deleted Items folder. --- ## Step 4: Configure the Field Nation connector Log in to Field Nation as a **Company Admin** and navigate to **Integrations → Field Services → Smartsheet**. ![Field Nation Smartsheet settings page showing Workspace, Template Sheet ID, Access Token, Notification Email, and Outbound Create fields](./images/smartsheet-settings.webp) Enter the following values: **Access Token** — Paste the API token from Step 1 directly into the Field Nation connector settings. Do not store it in this documentation. ### Outbound Create toggle | Setting | Behavior | | ----------------- | ------------------------------------------------------------------------------ | | **OFF** (default) | New work orders created in Field Nation will **not** create new Smartsheet rows. The connector only updates rows that are already linked via inbound sync. | | **ON** | Every new Field Nation work order creates a new row in your destination sheet. | Click **Save**. The connector validates your access token and sheet access automatically. If credentials are valid, the settings are persisted and the connector activates. --- ## Step 5: Test connection On save, the connector attempts to authenticate against the Smartsheet API and verify access to your Template Sheet. **Success:** Settings saved, connector active. **Failure — troubleshooting:** - Verify token was copied completely (no trailing spaces) - Confirm the token has not been revoked in Smartsheet → Apps & Integrations → API Access - Regenerate if needed — update immediately in Field Nation - Verify the Template Sheet ID is correct (numeric, 16 digits) - Ensure the token owner has Admin or Editor access to the sheet - If the sheet was moved or deleted, update the Sheet ID - Workspace ID is optional — try removing it - If using a workspace, verify the token owner has workspace access - Confirm the Workspace ID is correct (numeric) --- ## Step 6: Refresh fields Before configuring field mappings, click **Refresh Fields** to discover the available columns from your Template Sheet. **In Field Nation:** Integrations → Field Services → Smartsheet → Manage → **Refresh Fields** ![Field Nation toolbar showing Refresh Fields, Export, Import, Reset, and Delete actions](./images/smartsheet-toolbar-actions.webp) The connector performs the following behind the scenes: 1. Creates a temporary copy of your Template Sheet (to read the column schema) 2. Reads all column names, types, and options (picklist values, contact options) 3. Deletes the temporary sheet immediately 4. Populates the mapping UI with discovered fields > **Case sensitivity matters.** Column names are case-sensitive in the mapping UI. If your Smartsheet column is `Task Name`, you must select exactly `Task Name` — not `task name` or `TASK NAME`. > [INFO] **After changing your Template Sheet ID**, you must click Refresh Fields again. If you see "ghost" fields from the old sheet, use the **Toggle Dance** workaround: change the mapped dropdown to a blank field → Save → change it back to the correct field → Save again. ### System fields (automatically available) In addition to your sheet columns, the connector automatically provides these system mapping fields: | System field | Description | | ---------------------- | ---------------------------------------------------------- | | `sheet.id` | The Smartsheet Sheet ID — **required for Outbound Create** | | `sheet.name` | The sheet name | | `sheet.workspace.id` | The workspace ID (if sheet is in a workspace) | | `sheet.workspace.name` | The workspace name | | `sheet.row.id` | The Row ID within the sheet | --- ## Step 7: Configure field mappings Field mappings define how data translates between Smartsheet columns and Field Nation work order fields. From the Smartsheet settings page, click the **Field Mappings** tab (`Integrations → Field Services → Smartsheet → Field Mappings`). Mappings are fully customizable — you map whichever Smartsheet columns exist in your sheet to the corresponding Field Nation work order fields. The connector does not prescribe specific columns; it discovers whatever columns your Template Sheet contains (via Refresh Fields) and lets you map them freely. > [INFO] The only **required** mapping is `sheet.id` when Outbound Create is enabled. All other mappings are optional and depend on your sheet structure. ### Required mappings for Outbound Create When **Outbound Create** is enabled, the connector must know which sheet to create the new row in. This requires mapping the `sheet.id` system field. ![Field mapping UI showing Sheet ID mapped to sheet.id and Sheet URL with static value](./images/smartsheet-field-mapping.webp) > **`sheet.id` mapping is mandatory when Outbound Create is enabled.** Without it, the connector cannot determine which sheet to create the row in and outbound creates will fail silently. **How to configure:** 1. In the outbound mapping section, create a new mapping. 2. Set the Field Nation field to a custom field or static value containing your **destination Sheet ID**. 3. Set the Smartsheet field to `sheet.id` from the dropdown. 4. Save the mapping. ### Adding a mapping ### Select the source field For **inbound** mappings, select the Smartsheet source column from the left dropdown. For **outbound** mappings, select the Field Nation source field from the left dropdown. ### Select the target field Choose the corresponding field in the other system from the right dropdown. ### Choose the mapping action | Action type | When to use | | --------------------- | ---------------------------------------------------------- | | **Sync Values** | Values are identical or require no transformation | | **Array Map** | Value needs translation (e.g., dropdown label → status ID) | | **Date Convert** | Date format or timezone conversion | | **Concat Values** | Combine multiple fields into one | | **Set Static Values** | Always send the same value (e.g., fixed Sheet ID) | | **Custom Action** | Complex transformation via Jsonnet | ### Save Click **Save Mapping**. Repeat for all required fields. --- ## Step 8: Get your trigger URL The trigger URL is the endpoint Smartsheet webhooks will POST to when rows change. **Field Nation provides this URL** in the connector settings. **In Field Nation:** Integrations → Field Services → Smartsheet → **Trigger URL** The URL follows this pattern: ``` https://micro.fieldnation.com/v1/broker/inbound?client_token={your-client-token} ``` Copy and store the full URL including the client token. This token authenticates all inbound webhook requests — treat it as a secret. ![Field Nation Trigger Information section showing the Field Nation Sheet Notification URL (redacted)](./images/smartsheet-trigger-url.webp) > **You need this URL before setting up the Smartsheet webhook.** Complete this step, then proceed to [Workflow Setup](/docs/connectors/platforms/smartsheet/workflow) for webhook creation. --- ## Your reference sheet The values you collected above, in one place. The API access token is masked and never saved in your browser — enter it directly into the connector. --- ## Troubleshooting - Verify your Access Token has not expired or been revoked - Confirm the Template Sheet ID is correct and the sheet still exists - Check that the sheet has at least one column defined - Try clicking Refresh Fields again — transient API errors can cause one-time failures - If you recently changed the Template Sheet ID, use the Toggle Dance (see Step 6) This occurs when Field Nation holds cached column IDs from a previous Template Sheet. Fix with the Toggle Dance: 1. Change the affected mapping dropdown to a blank/random field 2. Click Save 3. Change it back to the correct Smartsheet column 4. Click Save again - Verify Outbound Create toggle is ON - Confirm `sheet.id` mapping exists and points to a valid destination Sheet ID - Check that the destination sheet's columns match your outbound mappings - Verify the Access Token has Editor access to the destination sheet - Click Refresh Fields to re-read the sheet schema - Verify the column type in Smartsheet matches what you expect (e.g., Dropdown vs Text) - Multi-select columns require `MULTI_PICKLIST` handling — values are comma-separated --- ## Next steps Configure the Smartsheet webhook to enable inbound sync. Enable bidirectional message sync between work order messages and Smartsheet row discussions. Sync files between Field Nation work orders and Smartsheet row attachments. --- ### Overview URL: /docs/connectors/platforms/smartsheet/overview The Field Nation Smartsheet connector links your Smartsheet workspace to Field Nation, keeping work orders and sheet rows synchronized between both platforms. When a row is ready for dispatch, the connector creates a work order in Field Nation automatically. When work progresses — status changes, messages posted, files uploaded — those updates flow back to your Smartsheet row in near real time. Smartsheet row changes create or update Field Nation work orders. Field Nation events push data back to your sheet rows. Work orders, row field values, messages/discussions, and attachments — mapped to any column type in your sheet. Smartsheet API Access Token. No OAuth flow required — generate directly from your Smartsheet account settings. Smartsheet webhooks notify Field Nation when rows change. Webhooks are created via the Smartsheet API (no UI option). --- ## Architecture The connector sits between Field Nation and Smartsheet. Inbound sync is triggered by Smartsheet webhooks — when a row changes, the webhook fires and Field Nation fetches the row data. Outbound sync is event-driven — when a work order is created or updated in Field Nation, the connector writes changes back to the linked Smartsheet row. ```mermaid --- config: flowchart: curve: linear --- flowchart LR subgraph SS["Smartsheet"] W([Webhook fires on row change]) R([Sheet rows + columns]) end subgraph CONN["FN–Smartsheet Connector"] direction TB IB["Inbound\nwebhook-driven"] OB["Outbound\nevent-driven"] end subgraph FN["Field Nation"] WO([Work orders]) EV([Work order events]) end W -->|"Row ID + Sheet ID"| IB R -->|"Fetch row data"| IB IB -->|"Field mappings applied"| WO EV -->|"Work order event"| OB OB -->|"Update/create row"| R ``` --- ## How sync works **Inbound (Smartsheet → Field Nation):** A Smartsheet webhook fires when a row is added or updated. The connector receives the Sheet ID and Row ID, fetches the full row data from the Smartsheet API, applies your field mappings, and creates or updates the corresponding Field Nation work order. **Outbound (Field Nation → Smartsheet):** When a Field Nation work order event fires (status change, message posted, attachment uploaded), the connector translates the work order data using your outbound mappings and writes it back to the linked Smartsheet row. ```mermaid sequenceDiagram participant SS as Smartsheet participant C as Connector participant FN as Field Nation Note over SS,FN: Inbound — Smartsheet → Field Nation SS->>C: Webhook fires (row changed) C->>SS: Fetch row data via REST API SS-->>C: Row columns + values C->>C: Apply field mappings C-->>FN: Create or update work order Note over SS,FN: Outbound — Field Nation → Smartsheet FN->>C: Work order event fires C->>C: Apply outbound mappings C->>SS: Update row via REST API SS-->>C: Row update confirmed C-->>FN: Sync complete ``` --- ## Use cases **Scenario:** Your team manages field service tasks in Smartsheet. When a row is ready for dispatch, you want a Field Nation work order created automatically without re-entering data. 1. A project manager adds or updates a row in Smartsheet. 2. They check the "Send to FN" checkbox (or set a status column to "Ready for Field"). 3. The Smartsheet webhook fires and sends the row ID to Field Nation. 4. The connector fetches the row, applies mappings (title, description, schedule, location), and creates a work order. 5. The work order is posted and a provider is dispatched. 6. As work progresses, status and completion data flow back to the Smartsheet row. **Scenario:** Your team needs real-time visibility into field work status directly inside Smartsheet without switching between platforms. - **Work order assigned** → Smartsheet row "Status" column updates to "Assigned" - **Work completed** → Smartsheet row "Status" column updates to "Complete", completion date populated - **Message posted** → New discussion created on the Smartsheet row with the message text No manual updates required — all driven by Field Nation work order events. **Scenario:** Your operations team wants end-to-end automation — rows in Smartsheet dispatch field work, and every new Field Nation work order also creates a corresponding row. Enable **Outbound Create** in connector settings. With this on: - Every new Field Nation work order creates a new row at the bottom of your destination sheet. - The row is linked via `-` for all future sync events. - Status changes, messages, and attachments flow to Smartsheet as work happens. - Inbound dispatch from Smartsheet rows continues to work on demand via webhook. This mode is best suited for teams where both platforms are active and need full parity. --- ## Supported operations | Data type | Inbound (SS → FN) | Outbound (FN → SS) | Notes | | ---------------------- | :----------------------------: | :-------------------------------: | --------------------------------------------------------------- | | Work order / Row | Create, Update | Create, Update | Core sync — field mappings applied | | Messages / Discussions | ✓ (comments → FN messages) | ✓ (FN messages → row discussions) | Controlled by `messages` service flag | | Attachments | ✓ (row attachments → FN files) | ✓ (FN files → row attachments) | Controlled by `attachments_import` / `attachments_export` flags | | Status | ✓ (column value → status) | ✓ (status → column value) | Via field mapping with array map action | | Auto-dispatch | ✓ | — | Optional — auto-assigns provider on inbound create | > [INFO] Attachment sync has a 29.9 MB file size limit for direct upload. Files larger than 29.9 MB are attached as URL links instead of embedded files. This is a Smartsheet API restriction. --- ## Supported column types The connector supports all standard Smartsheet column types. Each type is handled with appropriate serialization: | Smartsheet column type | Mapped data type | Behavior | | ---------------------- | ------------------ | --------------------------------------------------------- | | Text/Number | `string \| number` | Direct value pass-through | | Contact List | `select` | Mapped by email address | | Multi Contact List | `select` | Comma-separated emails | | Date | `string` | ISO date string | | DateTime | `string` | ISO datetime string | | Picklist (Dropdown) | `select` | Single value from options | | Multi Picklist | `select` | Comma-separated values, stored as `MULTI_PICKLIST` object | | Checkbox | `boolean` | Boolean — `true`/`false` | | Duration | `string \| number` | Numeric duration value | | Predecessor | `number` | Row dependency reference | > **Column limit:** The connector supports sheets with up to **400 columns**. Sheets exceeding this limit will fail during field refresh. This covers the vast majority of real-world sheets — Smartsheet's own maximum is 400 columns per sheet. --- ## Configuration stages ### Gather Smartsheet credentials Generate an API Access Token, identify your Template Sheet ID, and (optionally) your Workspace ID. No OAuth flow — token generation takes under 2 minutes. ### Configure the Field Nation connector Log in to Field Nation as a Company Admin, navigate to **Integrations → Field Services → Smartsheet**, and enter your credentials. The connector validates the token and sheet access on save. ### Refresh fields and configure mappings Click **Refresh Fields** to discover your sheet's columns. Then map Smartsheet columns to Field Nation work order fields for both inbound and outbound directions. ### Set up the Smartsheet webhook Create a webhook via the Smartsheet API that sends row change events to your Field Nation trigger URL. Enable the webhook to activate inbound sync. ### Test the integration Create or update a row in Smartsheet to verify inbound flow. Create a work order in Field Nation to verify outbound flow. --- ## Before you begin Complete the checks below before starting the [Configuration](/docs/connectors/platforms/smartsheet/configuration) steps. ### Your reference sheet Collect these values as you work through setup. Entries are saved in your browser and carry across the Smartsheet pages; the API access token is masked and never saved — enter it directly into the connector. --- ## Next steps Step-by-step setup: credentials, field mappings, and connection validation. Configure Smartsheet webhooks and enable inbound/outbound sync flows. Learn about mapping actions: sync values, array map, date convert, and more. --- ### Workflow URL: /docs/connectors/platforms/smartsheet/workflow The Smartsheet connector supports two independent sync directions. Each is configured separately and can be enabled independently based on your team's workflow. | Direction | How it works | Requires | | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- | | **Inbound** (Smartsheet → FN) | Smartsheet webhook notifies Field Nation when a row changes. The connector fetches the row data and creates/updates a work order. | Webhook created via Smartsheet API | | **Outbound** (FN → Smartsheet) | Field Nation events push data to the linked Smartsheet row automatically. | Connector configured + field mappings set | --- ## Inbound workflow (Smartsheet → Field Nation) ### How it works 1. A row in your Smartsheet is added or updated (e.g., a checkbox is checked or a status column changes). 2. The Smartsheet webhook fires and sends the Sheet ID + event details to your Field Nation trigger URL. 3. The connector authenticates to Smartsheet and fetches the full row data. 4. Field mappings are applied and a work order is created or updated in Field Nation. 5. The work order is linked via the external object ID (`-`) for all future sync. --- ### Step 1: Create a trigger column To prevent infinite loops (outbound updates triggering inbound sync, which triggers outbound again), create a dedicated column in your Smartsheet to act as the webhook trigger. **Recommended approach — "Send to FN" checkbox column:** 1. Open your active Smartsheet. 2. Add a new column: **Type:** Checkbox, **Name:** `Send to FN` 3. This column will be used in the webhook `subscope` to filter which changes trigger the webhook. ![Smartsheet rows with Send to FN checkbox column checked on rows 2 and 3](./images/smartsheet-send-to-fn-column.webp) > **Without a subscope column, the webhook fires on every change to any column.** This includes changes made by the outbound sync itself, creating an infinite loop. Always configure a subscope trigger column before enabling inbound sync. ### Step 2: Get the trigger column ID You need the numeric Column ID for the webhook subscope. Retrieve it via the Smartsheet API: ```bash curl -s "https://api.smartsheet.com/2.0/sheets/{YOUR_SHEET_ID}/columns" \ -H "Authorization: Bearer {YOUR_ACCESS_TOKEN}" \ | python3 -c " import json, sys columns = json.load(sys.stdin)['data'] for col in columns: if col['title'] == 'Send to FN': print(f\"Column ID: {col['id']}\") break " ``` ![GET request to Smartsheet columns endpoint in Postman](./images/columns-endpoint.webp) Or find it in the full JSON response — look for the column with `"title": "Send to FN"` and copy its `"id"` value. --- ### Step 3: Create the webhook > [INFO] Smartsheet webhooks **cannot be created via the Smartsheet UI**. You must use the REST API directly — via Postman, curl, or a script. Send a POST request to create the webhook: **Endpoint:** ``` POST https://api.smartsheet.com/2.0/webhooks ``` **Headers:** ``` Authorization: Bearer {YOUR_ACCESS_TOKEN} Content-Type: application/json ``` `{YOUR_CLIENT_TOKEN}` is from your Field Nation Trigger URL ([Configuration Step 8](/docs/connectors/platforms/smartsheet/configuration#step-8-get-your-trigger-url)). The token is the `client_token` query parameter in that URL. **Body:** ```json { "name": "FN Inbound Sync", "callbackUrl": "https://micro.fieldnation.com/v1/broker/inbound?client_token={YOUR_CLIENT_TOKEN}", "scope": "sheet", "scopeObjectId": {YOUR_SHEET_ID}, "subscope": { "columnIds": [8734792838472934] }, "events": ["*.*"], "version": 1 } ``` ![POST request to create webhook in Postman](./images/smartsheet-webhook-create.webp) ```bash curl -X POST "https://api.smartsheet.com/2.0/webhooks" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "FN Inbound Sync", "callbackUrl": "https://micro.fieldnation.com/v1/broker/inbound?client_token=YOUR_CLIENT_TOKEN", "scope": "sheet", "scopeObjectId": YOUR_SHEET_ID, "subscope": { "columnIds": [8734792838472934] }, "events": ["*.*"], "version": 1 }' ``` 1. Set method to **POST** 2. URL: `https://api.smartsheet.com/2.0/webhooks` 3. Headers: `Authorization: Bearer {token}`, `Content-Type: application/json` 4. Body (raw JSON): paste the JSON body above with your values 5. Click **Send** **Success response:** ```json { "message": "SUCCESS", "resultCode": 0, "result": { "id": 1234567890123456, "name": "FN Inbound Sync", "scope": "sheet", "scopeObjectId": {YOUR_SHEET_ID}, "subscope": { "columnIds": [8734792838472934] }, "events": ["*.*"], "callbackUrl": "https://micro.fieldnation.com/v1/broker/inbound?client_token=...", "enabled": false, "status": "NEW_NOT_VERIFIED", "version": 1 } } ``` Save the `id` from the response — this is your **Webhook ID** (needed for the enable step). > [INFO] **About `events: ["*.*"]`:** This catches all event types including row changes, comment additions, and cell updates. The Field Nation connector handles filtering internally — it processes row and comment events and ignores others. If you want to restrict at the Smartsheet level, use `["row.added", "row.updated"]` — **note: comment and discussion events are only delivered under `*.*` and cannot be filtered individually. Removing `*.*` means inbound message sync will not work.** --- ### Step 4: Webhook verification handshake When a webhook is created, Smartsheet sends a **verification request** to your callback URL. Field Nation's inbound endpoint handles this automatically — it responds with the expected verification payload. You do not need to do anything for this step. The verification completes within seconds of webhook creation. If verification fails: - Confirm your trigger URL is correct and includes the `client_token` parameter - Ensure the URL is publicly accessible (no VPN or firewall blocking) - Try creating the webhook again — transient network issues can cause verification timeouts --- ### Step 5: Enable the webhook Smartsheet creates all webhooks in a **disabled** state. You must explicitly enable it: ```bash curl -X PUT "https://api.smartsheet.com/2.0/webhooks/{WEBHOOK_ID}" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{"enabled": true}' ``` **Success response:** ```json { "message": "SUCCESS", "resultCode": 0, "result": { "id": 1234567890123456, "enabled": true, "status": "ENABLED" } } ``` ![Postman PUT request to enable webhook showing SUCCESS response](./images/smartsheet-webhook-enabled.webp) Once `status` is `ENABLED`, your inbound flow is live. Any change to the subscope column will trigger the webhook. --- ### Verify the webhook is active List all webhooks to confirm yours is enabled: ```bash curl "https://api.smartsheet.com/2.0/webhooks" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` Look for your webhook with `"enabled": true` and `"status": "ENABLED"`. --- ## Outbound workflows (Field Nation → Smartsheet) ### Work order create (Outbound Create) When **Outbound Create** is enabled and a new work order is created in Field Nation, the connector creates a new row at the bottom of your destination sheet. **Enable in:** Integrations → Field Services → Smartsheet → Settings — enable the **Outbound Create** toggle. ![Field Nation Outbound Create toggle enabled with Trigger Information section visible](./images/smartsheet-outbound-create-toggle.webp) > **Mandatory mapping:** You must have a `sheet.id` mapping configured that resolves to your destination Sheet ID. Without this, outbound create fails silently — no row is created and no error is raised. See [Required mappings for Outbound Create](/docs/connectors/platforms/smartsheet/configuration#required-mappings-for-outbound-create). **What happens on outbound create:** 1. A new work order is created in Field Nation. 2. The connector reads the outbound field mappings and translates work order data to Smartsheet column values. 3. A new row is created at the bottom of the destination sheet with the mapped values. 4. The external object ID (`-`) is stored on the work order for future sync. --- ### Work order update (Outbound Update) When a linked work order is updated in Field Nation, the connector updates the corresponding Smartsheet row. **No additional configuration required** — outbound updates happen automatically for any work order that has a linked external object ID. **Duplicate prevention:** Before writing an update, the connector compares the outbound mapped values against the current row data. If all values already match, no API call is made. This prevents unnecessary writes and avoids triggering your inbound webhook. **Events that trigger outbound updates:** - Work order status change - Work order field updates (title, description, schedule, etc.) - Message posted (if message sync is enabled) - Attachment uploaded (if attachment export is enabled) --- ### Message sync (bidirectional) When enabled, messages flow between Field Nation work order messages and Smartsheet row discussions. | Direction | Behavior | | ------------------- | -------------------------------------------------------------------------------------------------------------------------- | | **FN → Smartsheet** | A new message posted on a work order creates a new Discussion on the linked row with the message text as the first comment | | **Smartsheet → FN** | A new comment on a row discussion is synced to the work order as a message | Message sync is controlled by the `messages` service flag. See the [Message Sync guide](/docs/connectors/platforms/smartsheet/guides/message-sync) for full configuration details. --- ### Attachment sync (bidirectional) When enabled, files flow between Field Nation work order attachments and Smartsheet row attachments. | Direction | Behavior | | ------------------- | ------------------------------------------------------------------------ | | **FN → Smartsheet** | Files attached to a work order are uploaded to the linked Smartsheet row | | **Smartsheet → FN** | Row attachments are downloaded and attached to the work order | | File size | Upload method | | --------- | --------------------------------------------------- | | ≤ 29.9 MB | Direct file upload via Smartsheet API | | > 29.9 MB | Attached as a URL link (reference link to the file) | Attachment sync is controlled by two separate flags: - `attachments_import` — Smartsheet → Field Nation - `attachments_export` — Field Nation → Smartsheet See the [Attachment Sync guide](/docs/connectors/platforms/smartsheet/guides/attachment-sync) for full configuration details. --- ### Auto-dispatch When the `auto_dispatch` service flag is enabled, work orders created via inbound sync are automatically dispatched (assigned to a provider) without manual intervention. This is useful for teams that want fully automated end-to-end flow: row checked in Smartsheet → work order created → provider dispatched — all without human action in Field Nation. --- ## Testing the integration ### Test 1: Inbound (Smartsheet → Field Nation) ### Open your mapped Smartsheet Navigate to the sheet with the webhook configured. ### Update a row and trigger the webhook Fill in the mapped columns (e.g., Task Name, Description, Due Date) and check the **"Send to FN"** checkbox. ### Save the sheet Press Ctrl+S / Cmd+S to ensure changes are committed. ### Verify in Field Nation Navigate to your work orders. A new work order should appear within 30 seconds with the mapped data from your Smartsheet row. If nothing appears after 2 minutes, see [Webhook not firing](#webhook-not-firing) in the troubleshooting section below. --- ### Test 2: Outbound Create (Field Nation → Smartsheet) ### Create a new work order in Field Nation Fill in all fields that have outbound mappings configured (title, status, schedule, etc.). ### Route or publish the work order The outbound create triggers when the work order is fully created. ### Check your Smartsheet Scroll to the bottom of your destination sheet. A new row should appear with the work order data and a linked `-` relationship. --- ### Test 3: Outbound Update ### Edit a linked work order Open an existing work order that was created via Test 1 or Test 2. Edit a mapped field (e.g., change the title or status). ### Save the work order The outbound update triggers automatically. ### Check Smartsheet The corresponding row should update within 30 seconds with the new values. If nothing updates after 2 minutes, see [Outbound updates not reaching Smartsheet](#outbound-updates-not-reaching-smartsheet) in the troubleshooting section below. --- ## Troubleshooting - Verify your Access Token has permission to create webhooks (token owner must be sheet admin or owner) - Confirm the `callbackUrl` is publicly accessible - Check the `scopeObjectId` matches your Sheet ID exactly - Review the API error response for specific error codes - Verify the webhook is enabled: check `"status": "ENABLED"` via the list webhooks API - Confirm you're changing the subscope column (e.g., checking the "Send to FN" checkbox) - If `subscope.columnIds` is set, only changes to that column trigger the webhook - Check if the webhook was auto-disabled by Smartsheet (happens after repeated delivery failures) - Add a subscope column (checkbox) to prevent re-triggering on outbound updates - Verify you're not checking the "Send to FN" box on rows that already have linked work orders - The connector uses `-` as the external ID — if this link exists, it updates instead of creates - Verify the work order has a linked external object ID - Confirm outbound field mappings are configured for the fields being changed - Check that the Access Token still has Editor access to the destination sheet - The connector skips updates when values already match (duplicate prevention) — verify the Smartsheet row doesn't already have the expected values Smartsheet automatically disables webhooks after repeated delivery failures (e.g., if Field Nation's endpoint was temporarily unreachable). **To re-enable:** ```bash curl -X PUT "https://api.smartsheet.com/2.0/webhooks/{WEBHOOK_ID}" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{"enabled": true}' ``` If it fails with a verification error, the webhook needs to be recreated — Smartsheet may have invalidated the callback URL. This happens when: 1. A row change triggers inbound sync → creates/updates a work order 2. The work order update triggers outbound sync → updates the row 3. The row update triggers inbound sync again → loop **Fix:** Ensure your webhook `subscope` is set to a specific trigger column (e.g., checkbox). Outbound updates do not modify the trigger column, so the loop breaks. --- ## Smartsheet API rate limits Smartsheet enforces API rate limits that affect the connector: | Limit | Value | | ------------------------------------ | ----------------------------------- | | Requests per minute per access token | 300 | | Concurrent requests per access token | 10 | | Rows per addRows/updateRow call | 200 (connector sends 1 row per job) | The connector includes automatic retry logic with exponential backoff for `429 Too Many Requests` and `503 Service Unavailable` responses. Under normal usage, rate limits are not a concern. --- ## Next steps Enable bidirectional message sync between work order messages and row discussions. Configure file sync between work orders and Smartsheet row attachments. Deep-dive into mapping actions: sync values, array map, date convert, concat, and custom actions. --- ### Charge sync URL: /docs/connectors/platforms/autotask/guides/charge-sync > [INFO] This guide covers Autotask-specific charge sync configuration. For general field mapping concepts and transformation types, see [Field Mappings](/docs/connectors/concepts/field-mappings). ## Overview Configure your Autotask integration to sync Field Nation work order expenses directly to Autotask ticket **Charges** — giving you structured charge records on each ticket. On the **Additional Charges** page, select your sync target from the **Sync additional charges to** dropdown. When **Charges** is selected, a second dropdown — **Additional Charge ID Storage Field** — lets you choose which field on the Autotask Charge object stores the Field Nation charge ID used to track and sync future updates. Selecting **Expense** from the dropdown syncs expenses to Autotask Expenses instead. A dedicated guide covering Expense sync field mappings is coming soon. > [WARNING] **Switching from Expenses to Charges does not retroactively affect existing Autotask Expense records.** Only expenses synced after the change will create Charge records. Existing Autotask Expenses remain untouched. ## Prerequisites > [WARNING] Charge sync only works when your Autotask integration **object type is set to `ticket`**. If your object type is set to anything else, expenses will fail to sync and errors will appear in the event history log. Before proceeding, confirm: - The Autotask integration is already configured and authenticated - You have synced Autotask Fields using the "Refresh Fields" button - The integration object type is set to `ticket` in your Autotask connector settings ## Configuration ### Enable Charge Sync In your Autotask integration settings, navigate to the **Additional Charges** page on the left sidebar. ![The Autotask Integration settings sidebar with Additional Charges selected](./images/charge-sync-sidebar.webp) From the **Sync additional charges to** dropdown, select **Charges**. ![Switching the Sync additional charges to dropdown from Expenses to Charges in the Autotask Integration settings](./images/charge-sync-dropdown.gif) | Option | Behavior | | ----------- | ---------------------------------------------------------- | | **Expense** | Expenses sync to Autotask Expenses on the ticket (default) | | **Charges** | Expenses sync to Autotask Charges on the ticket | Once **Charges** is selected, a second dropdown appears below: **Additional Charge ID Storage Field**. | Option | Description | | ----------------------- | --------------------------------------------------------------- | | Notes | Stores the Field Nation charge ID in the Charge's Notes field | | Description | Stores the Field Nation charge ID in the Description field | | Purchase Order Number | Stores the Field Nation charge ID in the Purchase Order Number | | Internal PO Number | Stores the Field Nation charge ID in the Internal PO Number | > [INFO] **Choosing a storage field:** Select a field that is not already used by other integrations or internal processes. Avoid **Description** if you plan to map **Expense Item Description** in Step 2 — they write to the same field and will overwrite each other. The Field Nation charge ID stored here is used to track and sync future updates — such as edits, deletions, and disapprovals — back to the correct Autotask Charge record. > **Do not change this field after charges have synced.** The connector uses the stored ID to match existing Autotask Charge records. Changing the storage field after charges have been created will cause the connector to lose track of those records and may create duplicates on re-sync. ### Configure Field Mappings Still on the **Additional Charges** page, configure the following field mappings. **Charge Name**, **Category Code ID**, and **Amount** are required — without them, expenses will not sync. | Mapping Field | Required | Maps To in Autotask | | ------------------------ | -------- | ------------------------------------------------------------------------------------ | | Charge: Charge Name | Yes | The display name of the charge on the Autotask ticket. | | Charge: Category Code ID | Yes | The Material Code ID on the Autotask Charge object — typically set as a static value | | Charge: Amount | Yes | The unit cost on the Autotask Charge object | | Expense Item Description | No | An optional description on the Autotask Charge | > [INFO] **Finding your Category Code ID:** This value is not visible in the standard Autotask UI. You can retrieve it from the Autotask API or your Autotask admin settings. ![The Additional Charges field mapping UI showing Category mapped to Charge Name, Quantity mapped to Category Code ID with a static numeric value, Amount mapped to Charge Amount, and Description mapped to Expense Item Description](./images/charge-sync-field-mapping.webp) ### Test the Configuration Create a test expense on a Field Nation work order, then verify the result in Autotask: 1. The Charge appears on the corresponding Autotask ticket 2. The charge name, category, and unit cost match your field mapping configuration 3. The **Expense Item Description** is populated if you configured that mapping ![The Charges & Expenses tab on an Autotask ticket showing two synced charges — Extra Hours at $25.00 and Freight at $150.00 — with their names, amounts, and descriptions populated from Field Nation](./images/charge-sync-autotask-result.webp) ## How Charges Update Once a charge is synced to Autotask, Field Nation continues to keep it current. Changes to an expense on Field Nation are reflected on the corresponding Autotask Charge automatically. | Event on Field Nation | What happens in Autotask | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | Expense created | A new Charge is created on the Autotask ticket | | Expense deleted | The Charge is updated — cost and price are set to `0.00`, and the description is updated to indicate the expense was deleted on Field Nation | | Expense disapproved | The Charge is updated — cost and price are set to `0.00`, and the description is updated to indicate the expense was disapproved on Field Nation | ## Dynamic autotask categories It is possible to map different types of expenses to different autotask Charge Categories dynamically based on the expense label. This can be done through a custom action on the outbound **Charge: Category Code ID** mapping. ### Example custom action ```jsonnet // 1. Define a map of Expense label -> Autotask Category Code ID local expenseMap = { "Freight": 123456789, "Trip Charge": 134235124, "Taxes": 655365214, }; // Also define a default category ID. // Category labels not defined in the expenseMap will be put in this autotask charge category by default. // if there is no default category and we can't find a label match in the expenseMap, the charge sync will fail. local defaultCategoryId = 12345; // 2. Extract the target label from the expense results payload local targetLabel = $.input.pay.expense.results[0].label; // 3. Use the category ID dynamically if std.objectHas(expenseMap, targetLabel) then expenseMap[targetLabel] else defaultCategoryId ``` ### Changes in the Additional Charges mapping You can use any available field to map the custom action to Autotask's **Category Code ID** ![The Charge: Category Code ID mapping configured with a Custom Action that maps expense labels to Autotask category IDs using a lookup table with a fallback default](./images/charge-sync-custom-action.webp) ## Troubleshooting **Check your object name first.** Charge sync only works when the Autotask integration object type is `ticket`. If your integration is configured for any other object type, expenses will not create Charges in Autotask, and the sync failure is recorded in the **event history log**. To diagnose: 1. Confirm the object type is `ticket` in your Autotask connector settings 2. Open the **event history log** and look for sync errors — these will identify the exact failure 3. Verify all three required field mappings are configured: **Charge Name**, **Category Code ID**, and **Amount** 4. On the **Additional Charges** page, confirm **Sync additional charges to** is set to **Charges** (not **Expense**) 5. Confirm the **Additional Charge ID Storage Field** is set to a field not already used by other integrations or internal processes --- ### Time log sync URL: /docs/connectors/platforms/autotask/guides/time-log-sync > [INFO] This guide covers Autotask-specific mapping details. For general field mapping concepts and transformation types, see [Field Mappings](/docs/connectors/concepts/field-mappings). ## Time Log Synchronization When a provider logs time on a Field Nation work order, those time entries sync to Autotask's Time Entry section through the outbound integration. This includes the initial time log creation and any subsequent edits — if a provider updates their logged hours, the corresponding Autotask time entry updates automatically. ### Keeping Time Entries in Sync When a provider edits their logged time on Field Nation — adjusting hours, updating notes — that change needs to update the correct time entry in Autotask rather than creating a duplicate. To make this work, the connector tags each time entry with a reference ID from Field Nation. This is stored in one of Autotask's note fields (either Summary Notes or Internal Notes — your choice), so the connector always knows which entry to update. ## Time Log ID Storage Location You can choose which Autotask Time Entry field stores the time log ID: - **Summary Notes** (default) — The time log ID is stored in the Summary Notes field - **Internal Notes** — The time log ID is stored in the Internal Notes field ![Time Log ID Storage Location dropdown with tooltip explaining that Summary Notes must still be mapped when Internal Notes is selected](./images/time_log_storage.webp) > [WARNING] **If you select Internal Notes**, you must separately map Summary Notes to a Field Nation field. Summary Notes is a mandatory field in Autotask — if it's empty, time entries will not be created. ### How the Storage Format Works Regardless of which field you choose for the time log ID, both Summary Notes and Internal Notes can still be mapped to Field Nation fields. The stored value depends on whether the field has a mapping and whether it's used for time log ID storage: | Scenario | Stored Value | Example | |---|---|---| | Time log ID + mapped notes | `(#{id}) {mapped notes}` | `(#103) Replaced HVAC unit and tested airflow` | | Time log ID only (no mapping) | `(#{id})` | `(#104)` | | Mapped notes only (not used for ID) | `{mapped notes}` | `Verified network connectivity and rebooted router` | The connector uses the `(#{id})` prefix to identify which time entry to update when the time log is edited on Field Nation. ## Configuration ### Choose Your Storage Location In the Autotask connector configuration, locate the **Time Log ID Storage Location** dropdown and select either **Summary Notes** or **Internal Notes**. ### Map Fields (if using Internal Notes) If you selected **Internal Notes** as your storage location, you must map Summary Notes to a Field Nation field. Summary Notes is mandatory in Autotask — without a mapping, time entries will not be created. ![Field mapping showing Client Closing Notes mapped to TimeEntry Summary Notes and Internal Closing Notes mapped to TimeEntry Internal Notes](./images/field_mapping.webp) > [WARNING] This mapping is only required when Internal Notes is your Time Log ID storage location. If you're using the default Summary Notes storage, no additional mappings are required for time log sync to work. ### Test the Configuration Create a test work order in Field Nation's sandbox, add a time log, and verify: 1. The time entry appears in Autotask with the correct notes 2. The time log ID is stored in your chosen field (look for the `(#{id})` prefix) 3. Edit the time log on Field Nation and confirm the Autotask entry updates rather than creating a duplicate ## Summary | Storage Location | Summary Notes | Internal Notes | |---|---|---| | **Summary Notes** (default) | Stores time log ID (mapped value appended after ID) | Optional — mapped value only if configured | | **Internal Notes** | **Must be mapped** to a Field Nation field (mandatory for time entry creation) | Stores time log ID (mapped value appended after ID) | --- ### Callback setup URL: /docs/connectors/platforms/connectwise/guides/callback-setup A callback is the webhook that tells Field Nation a ticket is ready: when a ticket reaches your trigger status, ConnectWise posts the ticket ID to your Field Nation trigger URL, and the connector creates or updates the work order. > **Not familiar with REST APIs?** This step requires your ConnectWise administrator or a developer. Share this guide with them and hand off your Field Nation **Trigger URL** (from connector Settings) — it's a one-time setup. > **System callbacks are managed through the ConnectWise REST API — most instances have no Callbacks setup-table UI.** Cloud-hosted ConnectWise instances typically do not expose a callback management screen, and API members often lack permission to manage callbacks from the UI even when one exists. **This guide uses the REST API (Postman or cURL) as the primary, supported method.** A UI fallback is noted at the end for the rare instances that have it. > [INFO] Complete [Configuration Steps 1–5](/docs/connectors/platforms/connectwise/configuration) before continuing. You need your Field Nation trigger URL, your API credentials, and a ConnectWise `clientId` (generated below) to finish this guide. --- ## Prerequisites > [INFO] **Get a ConnectWise `clientId` first.** Every ConnectWise REST API call needs a `clientId` header — a value you generate on the ConnectWise Developer Network, which is separate from your ConnectWise PSA login: 1. Register or log in at [developer.connectwise.com](https://developer.connectwise.com) (not your PSA admin account). 2. Open **ClientID → Create New Integration** — Product **Manage**, Type **Private** (internal use). 3. Enter a name, description, and technical contact email, then **Submit**. 4. **Copy the generated Client ID** and store it securely — use it as the `clientId` header on every request below. Before registering the callback, collect these values: | Value | Where to find it | |---|---| | Field Nation trigger URL | Field Nation → Integrations → Field Services → ConnectWise → Manage → Settings → **Trigger URL**. Copy it **exactly** — it is environment-specific (see below) and contains a security token. | | ConnectWise host | Your instance host, e.g. `api-na.myconnectwise.net` | | Company | Your **login** company identifier (e.g. `acme_corp`) — see the [Company tip in Configuration Step 4](/docs/connectors/platforms/connectwise/configuration#step-4-generate-api-keys) for three ways to confirm the correct value. Used in API auth. | | Public Key | From the API member's API Keys tab | | Private Key | From the API member's API Keys tab | | ConnectWise `clientId` | Your own client ID from the ConnectWise Developer Network — generate it using the steps in the callout above. Sent as the `clientId` header on every API call. | | Target status name | The ConnectWise ticket status that should trigger inbound sync | > **The trigger URL is environment-specific — never assume the production URL.** It differs by Field Nation environment in both host and path. Always copy the exact value from your environment's Settings → Trigger URL: - **Production:** `https://micro.fieldnation.com/v1/broker/inbound?client_token=` - **Sandbox:** `https://micro.fndev.net/v1/broker-sandbox/inbound?client_token=` The `client_token` is a secret (it encodes your company + integration). --- ## Step 1: Set up authentication ConnectWise REST uses HTTP Basic auth (`Company+PublicKey:PrivateKey`) plus a `clientId` header on every request. Export these once so the examples below stay clean. Set `HOST` to your ConnectWise host, and replace `` (from Prerequisites), ``, ``, and `` with your own values: ```bash HOST='api-na.myconnectwise.net' B="https://$HOST/v4_6_release/apis/3.0" H_CLIENT='clientId: ' H_ACCEPT='Accept: application/vnd.connectwise.com+json; version=3.0.0' AUTH='+:' ``` curl base64-encodes `-u` into the `Authorization: Basic` header automatically. 1. Open a new request. 2. **Authorization** tab → type **Basic Auth**: - **Username:** `+` — concatenate with a literal `+` - **Password:** `` 3. **Headers** tab → add: - `clientId` → `` — your Developer Network client ID - `Accept` → `application/vnd.connectwise.com+json; version=3.0.0` - `Content-Type` → `application/json` (for the POST in Step 4) > A header explicitly set in the **Headers** tab overrides the **Authorization** tab. If a request seems to use the wrong credentials, open the **Postman Console** (View → Show Postman Console) and decode the actual `Authorization: Basic …` header to confirm which key pair is really being sent. --- ## Step 2: Discover the target status ID The callback payload needs the numeric **`objectId`** of your target status — not the status name. **Status IDs are unique per board**: the same status name has a different ID on each board. First, find the numeric **board ID** for the board your tickets live on. You need this to query statuses. ```bash curl -s -G "$B/service/boards" \ --data-urlencode 'fields=id,name' \ -H "$H_CLIENT" -H "$H_ACCEPT" -u "$AUTH" ``` - **Method:** `GET` - **URL:** `https:///v4_6_release/apis/3.0/service/boards` - **Params:** `fields` = `id,name` - Auth + headers inherited from Step 1. Find your board name and copy its `id` (e.g. `7`). Then list its statuses to get the numeric status ID. > ConnectWise returns **25 records per page** by default, and custom statuses often sort beyond the first page. Always append `pageSize=100` (or page through) or you may miss your target. ```bash curl -s -G "$B/service/boards//statuses" \ --data-urlencode 'pageSize=100' \ --data-urlencode 'fields=id,name' \ -H "$H_CLIENT" -H "$H_ACCEPT" -u "$AUTH" ``` - **Method:** `GET` - **URL:** `https:///v4_6_release/apis/3.0/service/boards//statuses` - **Params:** `pageSize` = `100`, `fields` = `id,name` - Auth + headers inherited from Step 1. Find your target status name and copy its top-level `id`: ```json { "id": 142, "name": "Dispatch to FN", "board": { "id": 7, "name": "Field Services" } } ``` Here `142` is the `objectId` for the next step. Repeat per board for multi-board setups. --- ## Step 3: Check for an existing callback (recommended) Before registering, list current callbacks so you don't create a duplicate (ConnectWise rejects a second callback on the same status with `ObjectExists`). **Use `pageSize=200`** — a recently added callback often sits on page 2 and won't appear in the default 25-record response, which makes it look like it doesn't exist. ```bash curl -s -G "$B/system/callbacks" \ --data-urlencode 'pageSize=200' \ --data-urlencode 'fields=id,description,url,objectId,level,type' \ -H "$H_CLIENT" -H "$H_ACCEPT" -u "$AUTH" ``` - **Method:** `GET` - **URL:** `https:///v4_6_release/apis/3.0/system/callbacks` - **Params:** `pageSize` = `200`, `fields` = `id,description,url,objectId,level,type` - Auth + headers inherited from Step 1. Scan the `url` values to see which environment each callback points at (`fieldnation.com` = production, `fndev.net` = sandbox) and which `objectId` (status) each watches. --- ## Step 4: Register the callback ```bash curl -i -X POST "$B/system/callbacks" \ -H "$H_CLIENT" -H "$H_ACCEPT" -H 'Content-Type: application/json' \ -u "$AUTH" \ -d '{ "description": "Field Nation Integration", "url": "&external_id=", "type": "ticket", "level": "status", "objectId": , "inactiveFlag": false, "isSoapCallbackFlag": false }' ``` - **Method:** `POST` - **URL:** `https:///v4_6_release/apis/3.0/system/callbacks` - **Body** → **raw** → **JSON**: ```json { "description": "Field Nation Integration", "url": "&external_id=", "type": "ticket", "level": "status", "objectId": , "inactiveFlag": false, "isSoapCallbackFlag": false } ``` Replace `` with your exact environment trigger URL (it already contains `client_token`; append `&external_id=` so ConnectWise appends the ticket ID when it fires) and `` with the integer from Step 2. > `objectId` must be a **bare integer** — `"objectId": 142` is correct; `"objectId": "142"` returns `400 Bad Request`. And `level: status` means the callback fires whenever **any** ticket on that board transitions **into** that status (not on tickets already sitting in it). --- ## Validate the callback ### Check the API response A successful registration returns **HTTP 201 Created**: ```json { "id": 88, "description": "Field Nation Integration", "url": "https://micro.fieldnation.com/v1/broker/inbound?client_token=&external_id=", "objectId": 142, "type": "ticket", "level": "status", "memberId": 15, "inactiveFlag": false, "isSoapCallbackFlag": false } ``` Note the returned `id` — the ConnectWise callback ID — in case you need to update or delete it later. ### Run an end-to-end test 1. Open a test ticket on your target board. 2. Change its status to your trigger status and **save** — that *transition* is what fires the callback (re-saving a ticket already in the status does nothing). 3. In ConnectWise: **System → Setup Tables → Integrator Login** → open your integration → confirm HTTP 200 in the callback log. 4. Check Field Nation — confirm a work order was created from the ticket. --- ## Error reference | HTTP code | Cause | Fix | |---|---|---| | `401 Unauthorized` / `Invalid Token` | Auth invalid, or **Company** is the display name instead of the login identifier | Confirm `+` format (no spaces), the Company is your **login** identifier, and the Private Key has no trailing whitespace | | `403 Forbidden` | API member lacks callback write permission | Use a member/role with callback management rights — many API roles can read (`GET`) callbacks but not add/delete them | | `400 Bad Request` (`InvalidObject`) | `objectId` quoted, or malformed JSON | `objectId` must be a bare integer; validate the JSON | | `400` `ObjectExists` ("A matching callback entry already exists") | A callback for that `objectId` is **already registered** | It already exists — re-run Step 3 with `pageSize=200` to find it (it may be on page 2). Update or reuse it instead of creating a new one | | `404 Not Found` | Path typo or wrong board/status ID | Check the endpoint path; re-run Step 2 to confirm the `objectId` | --- ## Troubleshooting - Confirm the connector's **Trigger On Status** value ([Configuration Step 5](/docs/connectors/platforms/connectwise/configuration#step-5-configure-the-field-nation-connector)) matches the ticket's status text exactly — same casing and spacing. The callback fires on the numeric `objectId`, but the connector independently re-checks the status *text* and silently skips tickets that don't match. **This is the most common cause.** - Confirm the ticket's board/status matches the callback's `objectId` exactly — a status with the same name on a different board has a different ID. - Confirm the trigger URL points at the **right environment** (`fndev.net` for sandbox, `fieldnation.com` for production) — a sandbox ticket hitting a production-pointed callback creates the work order in production. - Check Field Nation's integration **Event History** for the inbound job and any processing error (e.g. an invalid address/zip on the source ticket). - Confirm the connector is enabled in Field Nation settings. - You're almost certainly seeing only the first **25** results. Re-run the list with `pageSize=200` — newly added callbacks have higher IDs and land on a later page. - The same ConnectWise instance can have callbacks pointing at multiple Field Nation environments. A status change fires **every** callback registered for that status. - List all callbacks (`pageSize=200`) and check each `url` — disable or scope the ones for environments you're not testing, or use a distinct trigger status per environment. - `level: status` fires on every transition into the target status, including re-transitions. - Add a ConnectWise Workflow Rule using a custom field (e.g. `FN Sync Status`) to skip tickets already dispatched; set that field to a "sent" value via an outbound Field Nation mapping after the first sync. - Each board needs its own callback registration — one `objectId` per board status. - Re-run the status discovery query (`/service/boards//statuses?pageSize=100`) for each board and confirm the `objectId` matches that specific board's status. --- ## UI fallback (only if your instance has a Callbacks table) A minority of ConnectWise instances expose **System → Setup Tables → Callbacks**. If yours does, you can register there instead of the API: ### Open System → Setup Tables → Callbacks → **+** (New) ### Fill in the fields | Field | Value | |---|---| | Description | `Field Nation Integration` | | URL | Your Field Nation trigger URL (with `&external_id=` appended) | | Object Type | `Service Ticket` | | Level | `Status Level` (target the trigger status) | | Added / Changed | ✓ Checked | | Deleted / Inactive Members | Leave unchecked | ### Save and confirm it appears as **Active** If this table isn't present, or saving returns a permission error, use the API method above — that's the norm, not the exception. --- ## Summary | Item | Value / behavior | | --- | --- | | Primary method | REST API — `POST https:///v4_6_release/apis/3.0/system/callbacks` | | Auth | Basic `Company+PublicKey:PrivateKey` + `clientId` header (every request) | | `type` / `level` | `ticket` / `status` | | `objectId` | Numeric status ID — **unique per board**, bare integer (no quotes) | | `url` | Environment-specific Field Nation trigger URL (`fndev.net` sandbox / `fieldnation.com` prod) + `&external_id=` | | Listing callbacks | Always `pageSize=200` — the default 25 hides newer callbacks | | Already exists | `400 ObjectExists` means it's registered — find it (page 2), don't recreate | | Fires when | A ticket **transitions into** the target status | | Multi-board | One callback per board status | | UI method | Only on instances with a Callbacks setup table — usually unavailable | --- ### Parts sync URL: /docs/connectors/platforms/salesforce/guides/parts-sync > [INFO] This guide covers Salesforce-specific parts sync configuration. For general field mapping concepts and transformation types, see [Field Mappings](/docs/connectors/concepts/field-mappings). ## Overview Sync parts data between Field Nation work orders and Salesforce — when a technician adds, updates, or receives parts on a work order, those records automatically appear on the associated Salesforce Case (or custom object). Inbound sync from Salesforce to Field Nation is also supported. This integration eliminates manual data entry for: - Part numbers and descriptions - Shipping carriers and tracking IDs - Return logistics (carrier, tracking, instructions) - Returnability status ## Prerequisites Before proceeding, confirm: - The Salesforce connector is [configured and authenticated](/docs/connectors/platforms/salesforce/configuration) - You have **Salesforce Administrator** access to create custom objects and fields - Your integration user profile has Read, Create, and Edit permissions on the Parts object (see [Profile Permissions](#profile-permissions) below) --- ## Phase 1: Salesforce Setup Field Nation dynamically reads your Salesforce architecture. You must build the custom object and fields in Salesforce **before** mapping them in Field Nation. ### Create the Parts Custom Object In Salesforce Setup, create a dedicated object to hold parts data. 1. Click the **Gear Icon** → **Setup** 2. Navigate to **Object Manager** → **Create** → **Custom Object** 3. Configure these settings: | Setting | Value | | --------------- | ----------------------------------- | | Label | Part | | Plural Label | Parts | | Object Name | Part (generates API name `Part__c`) | | Record Name | Part | | Data Type | Auto Number | | Display Format | `P-{000000}` | | Starting Number | 1 | 4. Click **Save** > [INFO] You can use a custom object name, but it must be mapped correctly in Phase 3. We recommend `Part__c` for consistency with this guide. ### Link the Parts Object to Your Primary Object Create a Lookup Relationship so Parts appear as a Related List on your Case (or primary work order object). 1. In **Object Manager**, select your new **Part** object 2. Click **Fields & Relationships** → **New** 3. Select **Lookup Relationship** → click **Next** 4. Related To: select **Case** (or your primary object) → click **Next** 5. Field Label: enter `Case` → click **Next** 6. **Field-Level Security**: ensure your integration user profile has **Visible** checked 7. Leave default checkboxes for page layouts → click **Save** > [WARNING] **Do not skip Field-Level Security.** If the integration user cannot see this relationship field, parts will fail to sync and errors will appear in the event history log. ### Build the Parts Fields Create the following custom fields on the Parts object. For each field: select the Data Type, enter the Field Label, set the Length, and grant **Read/Edit** access to your integration user profile. | Field Label | Data Type | Length / Notes | | -------------------- | --------- | --------------------------------------------- | | Part ID | Text | 255 — Stores the Field Nation part identifier | | Part Description | Text Area | Standard | | Part Number | Text | 255 | | Usage Code | Text | 255 | | Incoming Tracking ID | Text | 255 | | Incoming Carrier | Text | 255 | | Return Tracking ID | Text | 255 | | Return Carrier | Text | 255 | | Is Returnable | Picklist | Values: `Yes`, `No` | | Return Instructions | Text Area | Standard | | Return Issue | Text | 255 | Use **Save & New** to move through the list quickly. ### Profile Permissions Your Salesforce integration user must have object-level permissions on the Parts object. To verify or update: 1. **Setup** → search **Profiles** → select your integration user profile 2. Click **Object Settings** → find **Parts** 3. Click **Edit** and enable: - ☑ Read - ☑ Create - ☑ Edit 4. Click **Save** > [WARNING] If you skip this step, the integration will authenticate successfully but fail to create Part records. Check the **event history log** if parts are not syncing. --- ## Phase 2: Refresh Fields in Field Nation After building the Salesforce object and fields, tell Field Nation to fetch your updated schema. ### Open Integration Settings Log in to Field Nation and navigate to your Salesforce integration settings. ### Refresh the Connection In the **Integration Details** section (left-hand navigation), click **Refresh Fields**. Field Nation queries your Salesforce instance and pulls in the new Parts object and its fields, making them available for mapping. > [INFO] Field refresh typically completes in under 30 seconds. If the new fields do not appear after refreshing, verify your integration user has the correct profile permissions. > [WARNING] All fields must be successfully received by Field Nation before they appear on the Mapping pages. After refreshing, confirm that every expected field is listed. If any are missing, review your Salesforce field-level security settings and re-run the refresh. --- ## Phase 3: Configure Field Mappings With Field Nation now aware of your Salesforce Parts fields, link them together in the mapping UI. ### Navigate to Parts Mappings In the Salesforce Integration settings, look under **Work Order Mappings** in the left-hand menu and click **Parts**. ### Map Fields Using the visual mapping interface, link Field Nation fields (left) to your Salesforce fields (right). Configure the direction arrows exactly as shown in the screenshot below to ensure the data pushes correctly to Salesforce. ![The Salesforce Integration Parts mapping UI showing all 11 fields mapped between Field Nation and Salesforce with bidirectional arrows configured — Part ID, Part Description, Part Number, Usage Code, Incoming Tracking ID, Incoming Carrier, Return Tracking ID, Return Carrier, Is Returnable, Return Instructions, and Return Issue](./images/parts-field-mapping.webp) > [INFO] **Part ID is required.** This field is the unique identifier that prevents duplicate records. Without it, updating a part on Field Nation will create a new Salesforce record instead of updating the existing one. --- ## Phase 4: End-to-End Testing Validate that parts sync correctly in both directions and that updates modify existing records rather than creating duplicates. ### Test 1: Outbound (Field Nation → Salesforce) ### Create a Part on Field Nation Open a test work order in Field Nation. Add a new part with these details: - Part Number - Description - Incoming Carrier - Incoming Tracking ID ### Verify in Salesforce Open the associated Case in Salesforce and check the **Parts** Related List. The new part should appear with all mapped fields populated. ### Test Updates Go back to Field Nation, update the tracking number on that same part, and verify the **existing** Salesforce record updates — it should not create a duplicate. ### Test 2: Inbound (Salesforce → Field Nation) ### Create a Part in Salesforce Navigate to your test Case and use the **Parts** Related List to create a new Part record directly in Salesforce. ### Verify in Field Nation Open the linked work order in Field Nation and confirm the new part appears in the **Parts** section with the correct field values. > [WARNING] **Inbound sync requires a configured Salesforce Flow or trigger** that notifies Field Nation when a Part record is created or updated. Without this, Salesforce-to-Field Nation sync will not fire automatically. --- ## How Parts Update Once a part is synced, Field Nation keeps it current. The Part ID field is the key that links records across systems. | Event | Result | | ---------------------------- | ---------------------------------------------------------------------------- | | Part added on Field Nation | New Part record created on the Salesforce Case | | Part updated on Field Nation | Existing Salesforce Part record is updated (matched by Part ID) | | Part added in Salesforce | New part appears on the Field Nation work order (if inbound sync configured) | | Part updated in Salesforce | Existing Field Nation part is updated (if inbound sync configured) | --- ## Troubleshooting 1. **Check profile permissions** — The integration user must have Read, Create, and Edit on the Parts object 2. **Verify field-level security** — The Lookup Relationship field must be visible to the integration user 3. **Confirm Refresh Fields was run** — Field Nation cannot map to fields it hasn't discovered 4. **Check the event history log** — Look for specific error messages indicating which field or permission is failing 5. **Verify Part ID mapping** — Without it, the connector cannot match records This happens when the **Part ID** field is not mapped. Without Part ID, the connector cannot identify existing records and creates a new one on every sync. **Fix:** Map the Part ID field (Field Nation → Salesforce) and ensure the Salesforce field is populated on existing records. After creating new fields in Salesforce: 1. Confirm the field was saved (check Object Manager → Part → Fields & Relationships) 2. Verify field-level security grants visibility to the integration user 3. Click **Refresh Fields** in the Field Nation integration settings 4. Wait 30 seconds and reload the mapping page Inbound sync requires an active Salesforce Flow or trigger that sends notifications to Field Nation when Part records change. Verify: 1. A Record-Triggered Flow exists on the Part object 2. The Flow calls an Outbound Message pointing to your Field Nation trigger URL 3. The Flow's conditions are met (e.g., record is created or updated) 4. Check the bidirectional direction arrows (↔ or ←) are set on the relevant field mappings --- ## Next Steps - [Field Mappings](/docs/connectors/concepts/field-mappings) — Learn about transformation types and custom actions - [Events and Sync](/docs/connectors/concepts/events-and-sync) — Understand how sync events trigger and retry - [Salesforce Workflow Setup](/docs/connectors/platforms/salesforce/workflow) — Configure Flows and Outbound Messages for inbound data --- ### Australia release upgrade URL: /docs/connectors/platforms/servicenow/guides/australia-release-upgrade > [INFO] This guide covers a ServiceNow-specific upgrade precaution. For connector setup and roles, start with the [Overview](/docs/connectors/platforms/servicenow/overview) and [Configuration](/docs/connectors/platforms/servicenow/configuration) pages. ## The short version Migrating your ServiceNow instance to the **Australia** release does **not** break the Field Nation integration on its own. The risk is in the migration *process*: it can move your integration's API user out of the **global** domain. When that happens, the user silently loses the permissions it needs, data stops flowing from Field Nation into ServiceNow, and you see what looks like an authentication or business-rule error. **Two things are required when migrating to Australia:** 1. **Keep the API user on the global domain** — or restore them to the domain they were on before the migration, where their permissions were intact. 2. **Add `useraccount` to the OAuth Auth Scopes** in the ServiceNow Application Registry record. > Before migrating to Australia, note your Field Nation API user's current domain and confirm it again afterward. The user must remain on **global** (or their pre-migration domain) — any domain change during the migration causes a silent permission loss that breaks the integration. You must also add `useraccount` to the OAuth **Auth Scopes** in the Application Registry as part of the migration. ## Symptoms After migrating a sandbox or production instance to Australia, the integration stops working in one or more of these ways: - **Authentication appears to fail** when you click **Save** on the Field Nation ServiceNow settings page. - **No data flows from Field Nation into ServiceNow** — work order updates made in Field Nation don't appear on the ServiceNow record. - **A business rule validation error**, most commonly from the out-of-the-box **`Populate Group - Dispatch/Work`** rule, which blocks the update. - **Assignment Group / Assigned To fields don't update** (or don't clear) as expected. > [INFO] These look like three different problems — authentication, business rule, field mapping — but they almost always trace back to a single cause: the API user's domain changed during the migration. ## Root cause: the API user left the **global** domain ServiceNow uses **domain separation** to partition data and permissions. A user on the **global** domain can see and act on records across the instance. A user moved into a sub-domain (for example, `TOP/ Data Integrity`) only sees what that sub-domain allows. During an Australia migration, the Field Nation API user can be moved out of **global** into a sub-domain. Once that happens, the user loses permission to read valid **assignment groups**. The out-of-the-box **`Populate Group - Dispatch/Work`** business rule then fails its validation — and because that rule blocks the write, every Field Nation → ServiceNow update fails. ```mermaid flowchart TD A[Australia migration] --> B[User leaves global domain] B --> C[Loses assignment group access] C --> D[Populate Group rule fails] D --> E[FN to ServiceNow sync blocked] ``` ## The fix ### Return the API user to the **global** domain (or their pre-migration domain) 1. In ServiceNow, go to **User Administration → Users** and open the Field Nation integration user record. 2. Find the **Domain** field on the form. If it isn't shown, add it: right-click the form header and choose **Configure → Form Layout**, move **Domain** (and **Managed Domain**, if present) from **Available** to **Selected**, then **Save**. 3. Set **Domain** to **global** or back to the value **it had before the migration**. 4. Confirm **Managed Domain** is **true** (if your instance shows this field). 5. Right-click the form header and click **Save**. ![ServiceNow integration user record with the Domain field set to global and Managed Domain set to true](./images/servicenow_user_global_domain.webp) > [INFO] Menu labels vary slightly by ServiceNow release. If you don't see **Configure → Form Layout**, use the gear / personalize icon on the form header to add the **Domain** field. ### Add `useraccount` to the OAuth Auth Scopes The Australia release requires `useraccount` to be present in the **Auth Scopes** section of the OAuth Application Registry record. 1. In ServiceNow, navigate to **System OAuth → Application Registry** and open the Field Nation OAuth application record. 2. Scroll to the **Auth Scopes** section at the bottom of the form. 3. Click **+ Insert a new row...** and type `useraccount` in the **Auth Scope** field. 4. Click **Submit** to save the record. ![ServiceNow Application Registry record showing the Auth Scopes section at the bottom of the form, with a new row being added for useraccount](./images/servicenow_auth_scopes.webp) ### Reset the password and company if prompted Moving a user between domains can require re-setting the password and re-selecting the company. If so, set them — then update the same credentials on the Field Nation [settings page](/docs/connectors/platforms/servicenow/configuration#step-3-authentication). ### Remove temporary mapping workarounds If anyone hard-coded **Assignment Group** or **Assigned To** values in the Field Nation integration mappings while troubleshooting, remove them. They are not needed once the domain is correct, and they mask the real behavior. Field mapping should look exactly as it did before the migration. ### Verify the round trip Run the [verification steps](#after-you-migrate) below to confirm both directions sync. > Do **not** disable or modify the `Populate Group - Dispatch/Work` business rule to work around this. It fails because of the permission loss, not because the rule itself is wrong. Fixing the domain fixes the rule. ## What is **not** the cause When the integration breaks right after a migration, it's tempting to assume the new release changed the API contract. In this case it didn't: - **The integration uses the OAuth 2.0 password (ROPC) grant** and constructs the token URL automatically — there is no token-URL field to update. However, you must add `useraccount` to the OAuth **Auth Scopes** on the Application Registry record as part of the Australia migration. - **The Australia release itself is compatible.** Independent sandbox testing on Australia confirmed authentication, field refresh, work order title updates, and work order link sync all worked normally. ## Before you migrate Run this on your **sandbox** first, then production: - Note your API user's current domain. If it is **global**, it must stay **global** after the migration. - Record which roles the user has (e.g., `wm_dispatcher`, and any others your setup requires). See the [roles matrix](/docs/connectors/platforms/servicenow/overview#2-create-a-dedicated-service-account). - Confirm the integration is syncing normally *before* the migration so you have a clean baseline. - Plan to add `useraccount` to the OAuth **Auth Scopes** on the Application Registry record as part of the migration — this is required for Australia. ## After you migrate > [INFO] Run these in order. If step 1 passes but step 2 fails, the problem is almost certainly the domain — go to [The fix](#the-fix). ### Test authentication Open the Field Nation ServiceNow settings page and click **Save**. A successful save means OAuth is working. - **Sandbox**: `https://ui-sandbox.fndev.net/integrations/fieldservices/servicenow/settings` - **Production**: `https://app.fieldnation.com/integrations/fieldservices/servicenow/settings` ![Field Nation ServiceNow integration settings page, clicking Save to confirm authentication succeeds](./images/servicenow_settings_save.webp) > Before testing, confirm you have completed [Step 2 of the fix](#the-fix): the OAuth Application Registry **Auth Scopes** must include `useraccount` for Australia. If this step fails after confirming the auth scope, see the [Authentication fails FAQ](#authentication-fails-when-i-click-save). ### Test ServiceNow → Field Nation Create a Work Order Task (`wm_task`) in ServiceNow and send it to Field Nation. Confirm the work order is created. ### Test Field Nation → ServiceNow Update a work order from the Field Nation side and confirm the change flows back to the ServiceNow record without a business-rule error. If all three pass, the migration is complete and the integration is healthy. ## FAQ Check the API user's domain first. Open the integration user record in ServiceNow and confirm **Domain = global** with **Managed Domain = true**. A domain change during the upgrade is the most common cause. If the domain is correct, work through the [verification steps](#after-you-migrate). This out-of-the-box rule blocks the write because the API user can no longer read valid assignment groups — a permission lost when the user left **global**. Return the user to **global** (see [The fix](#the-fix)). Do **not** deactivate the rule; it will simply fail again once data depends on it. First, confirm `useraccount` has been added to the **Auth Scopes** section of the OAuth Application Registry record — this is required for Australia and is the most common cause of auth failure post-migration. Open **System OAuth → Application Registry**, find the Field Nation application record, scroll to **Auth Scopes**, and check that `useraccount` appears. If it doesn't, click **+ Insert a new row...**, add it, and submit. See [Step 2 of the fix](#the-fix) for the full walkthrough with screenshots. Then confirm the **Instance Name** is the subdomain only (e.g., `dev12345`, not the full URL), and that the OAuth Client ID/Secret and service-account credentials still match the Application Registry record. If the user's password was reset during a domain move, update it on the settings page. See the full [401 troubleshooting steps](/docs/connectors/platforms/servicenow/configuration#troubleshooting-connection-errors). If those fields were hard-coded in the integration mappings during troubleshooting, remove the overrides — they are not needed once the domain is correct. With the user back on **global**, the standard mappings handle these fields normally. Yes. Open **System OAuth → Application Registry** in ServiceNow, find the Field Nation application record, scroll to the **Auth Scopes** section at the bottom, click **+ Insert a new row...**, and add `useraccount`. This is required on every Australia migration — not just a fallback for when auth breaks. See [Step 2 of the fix](#the-fix) for the full walkthrough with screenshots. The failure is tied to the user's domain changing during a migration, not to a specific release. An older, stable release that hasn't been migrated is not affected. Apply the [pre-migration checklist](#before-you-migrate) before you move production to Australia. ## Still stuck? If the integration still isn't syncing after confirming the API user is on the **global** domain, [create a support ticket](https://support.fieldnation.com) or reach out to your **Account Manager**, who can loop in a Field Nation solution engineer to troubleshoot. --- ### Attachment sync URL: /docs/connectors/platforms/smartsheet/guides/attachment-sync Attachment sync moves files between Field Nation work orders and Smartsheet row attachments automatically. When a provider uploads a deliverable or photo to a work order, it appears on the linked Smartsheet row. When a project manager attaches a document to a row, it becomes available on the work order. > [INFO] This guide covers Smartsheet-specific attachment sync. For general concepts on how events and sync work across connectors, see [Events & Synchronization](/docs/connectors/concepts/events-and-sync). --- ## How it works | Direction | Trigger | Behavior | | ------------------------------ | --------------------------- | ----------------------------------------------------------------------------------- | | **Inbound** (Smartsheet → FN) | Inbound create or update | All attachments on the Smartsheet row are downloaded and attached to the work order | | **Outbound** (FN → Smartsheet) | Work order attachment event | The uploaded file is sent to the linked Smartsheet row as a row attachment | ```mermaid sequenceDiagram participant FN as Field Nation participant S3 as Amazon S3 participant C as Connector participant SS as Smartsheet Note over FN,SS: Inbound — Smartsheet attachment → FN work order SS->>C: Row data includes attachments C->>SS: Fetch attachment metadata C->>SS: Download attachment binary C->>S3: Upload to transient storage C-->>FN: Attach file to work order Note over FN,SS: Outbound — FN attachment → Smartsheet row FN->>C: Attachment event fires C->>FN: Download file from FN alt File ≤ 29.9 MB C->>SS: Upload file directly (addRowFileAttachment) else File > 29.9 MB C->>SS: Attach as URL link (reference) end SS-->>C: Attachment confirmed ``` --- > **Account enablement required.** Attachment sync is disabled by default and cannot be self-enabled. Contact your Field Nation account team to enable it before continuing. Nothing in this guide will work until that is done. ## Prerequisites - [Smartsheet connector configured](/docs/connectors/platforms/smartsheet/configuration) and active - Work order linked to a Smartsheet row (via inbound create or outbound create) --- ## Enable attachment sync Attachment sync is controlled by two independent service flags: | Flag | Direction | Location | | -------------------- | ------------------------- | ------------------------------------------------------ | | `attachments_import` | Smartsheet → Field Nation | Integrations → Smartsheet → Manage → Attachment Import | | `attachments_export` | Field Nation → Smartsheet | Integrations → Smartsheet → Manage → Attachment Export | You can enable one or both directions independently. ![Field Nation attachment sync toggles — import all files from Smartsheet and send provider uploads to Smartsheet](../images/smartsheet-attachment-flags.webp) --- ## Inbound: Smartsheet attachments → Field Nation When an inbound create or update job runs and `attachments_import` is enabled: 1. The connector fetches the list of attachments on the Smartsheet row. 2. For each attachment, it retrieves the attachment metadata and download URL. 3. The file is downloaded and uploaded to Amazon S3 (transient storage). 4. The file is then attached to the Field Nation work order. **All row attachments are synced** — including files uploaded by any user, not just the one that triggered the sync. ### Attachment visibility Configure whether imported attachments are public (visible to assigned providers) or private (internal only): | Setting | Behavior | | ------------------- | ---------------------------------------------- | | `private` (default) | Attachments are visible only to the buyer team | | `public` | Attachments are visible to assigned providers | This is controlled by the `attachments_public` service flag. --- ## Outbound: Field Nation attachments → Smartsheet When a file is attached to a linked work order and `attachments_export` is enabled: 1. The connector downloads the file from Field Nation. 2. Based on file size, it chooses the upload method: | Condition | Method | Result in Smartsheet | | -------------- | ------------------- | --------------------------------------- | | File ≤ 29.9 MB | Direct file upload | Embedded file attachment on the row | | File > 29.9 MB | URL link attachment | Reference link to the Field Nation file | 3. The attachment is linked via an object relationship (`file_uid`) for duplicate prevention. ### Duplicate prevention The connector tracks which files have been synced using the file's unique upload ID. If a file has already been exported to Smartsheet (matching `upload_unique_id`), it will not be sent again — even if the outbound update event fires multiple times. ### File naming Exported files are renamed with a unique suffix to prevent collisions: ``` original_filename_{unique_id}.extension ``` Example: `site_photo_abc123def.jpg` --- ## Limitations | Limitation | Details | | ------------------------------- | ----------------------------------------------------------------------------- | | File size cap for direct upload | 29.9 MB — larger files become URL links | | Outbound create attachments | Currently disabled — attachments only sync on outbound **update** events | | File type restrictions | No restrictions from the connector side — Smartsheet may reject certain types | | Attachment count | All row attachments are synced (no filtering by type or count) | > [INFO] **Outbound create limitation:** When a work order is first created and Outbound Create adds a row, attachments that were added during creation are not automatically exported. Attachments are only exported on subsequent update events (e.g., when a new file is uploaded after the work order is linked). This is by design to prevent large initial sync payloads. --- ## Troubleshooting - Verify `attachments_import` flag is enabled for your connector - Confirm the Smartsheet row actually has attachments (check in Smartsheet UI) - Check that the Access Token has permission to download attachments from the sheet - Review connector logs for S3 upload errors - Verify `attachments_export` flag is enabled - Confirm the work order has a valid linked external object ID - Check file size — files >29.9 MB appear as URL links, not embedded files - Verify the Access Token has Editor access to the sheet (required for adding attachments) - Check if the file was already synced (duplicate prevention may be blocking it) This is expected behavior. Smartsheet's API has a practical upload limit, and the connector uses 29.9 MB as the threshold. Files larger than this are attached as URL reference links pointing to the Field Nation download URL. The file is still accessible — it just requires clicking the link to download. - Check the `attachments_public` service flag setting - Imported attachments default to private (internal only) - Change the flag to `public` if providers need to see imported documents --- ## Summary | Configuration | Inbound (Smartsheet → FN) | Outbound (FN → Smartsheet) | | --- | --- | --- | | `attachments_import` **OFF** | No sync | — | | `attachments_import` **ON** | All row attachments imported to work order | — | | `attachments_export` **OFF** | — | No sync | | `attachments_export` **ON**, file ≤ 29.9 MB | — | File uploaded directly to Smartsheet row | | `attachments_export` **ON**, file > 29.9 MB | — | URL reference link attached to row | | `attachments_public` **OFF** (default) | Imported files visible to buyer team only | — | | `attachments_public` **ON** | Imported files visible to assigned providers | — | --- ### Message sync URL: /docs/connectors/platforms/smartsheet/guides/message-sync Message sync connects Field Nation work order messages with Smartsheet row discussions. When a provider or buyer posts a message on a work order, it appears as a new discussion comment on the linked Smartsheet row. When someone adds a comment to a row discussion, it syncs back to the work order as a message. > [INFO] This guide covers Smartsheet-specific message sync. For general concepts on how events and sync work across connectors, see [Events & Synchronization](/docs/connectors/concepts/events-and-sync). --- ## How it works Smartsheet has a two-level messaging structure: **Discussions** (threads) and **Comments** (individual messages within a thread). Field Nation uses flat messages on work orders. The connector maps between them as follows: | Direction | Behavior | | ------------------- | -------------------------------------------------------------------------------------------------------------------- | | **FN → Smartsheet** | Each FN work order message creates a **new Discussion** on the linked row with the message text as the first comment | | **Smartsheet → FN** | A new comment on a row discussion creates a message on the linked work order | ```mermaid sequenceDiagram participant FN as Field Nation participant C as Connector participant SS as Smartsheet Note over FN,SS: Outbound — FN message → Smartsheet discussion FN->>C: message_posted event C->>SS: Create row discussion (POST) SS-->>C: Discussion ID + Comment ID C-->>FN: Object relationship stored Note over FN,SS: Inbound — Smartsheet comment → FN message SS->>C: Webhook fires (comment event) C->>SS: Fetch comment details C->>SS: Fetch discussion (get parent row) C-->>FN: Create message on linked work order ``` --- ## Prerequisites - [Smartsheet connector configured](/docs/connectors/platforms/smartsheet/configuration) and active - [Webhook configured](/docs/connectors/platforms/smartsheet/workflow) with `events: ["*.*"]` (must include comment events) - Work order linked to a Smartsheet row (via inbound create or outbound create) --- ## Enable message sync Message sync is controlled by the `messages` service flag in your connector settings. **In Field Nation:** Integrations → Field Services → Smartsheet → Manage → **Messages** toggle ![Field Nation Messages section with Send Work Order messages to Smartsheet toggle enabled](../images/smartsheet-message-sync-toggle.webp) | Setting | Behavior | | ----------------- | ----------------------------------------------------------------- | | **OFF** (default) | No message sync in either direction | | **ON** | Messages sync bidirectionally between linked work orders and rows | ### Message visibility Configure whether synced messages are public (visible to assigned providers) or private (internal only): | Setting value | FN message type | | ------------------- | ------------------------------------------------------------------------------------------------------------------- | | `private` (default) | `INTERNAL` — visible only to buyer team | | `public` | Visible to providers — exact audience depends on work order state: requested providers, assigned provider, or routed providers | --- ## Outbound: FN messages → Smartsheet discussions When the `message_posted` event fires on a linked work order: 1. The connector checks if message sync is enabled. 2. If enabled and the message text is not empty, it creates a new **Row Discussion** on the linked Smartsheet row. 3. The discussion's first comment contains the message text. 4. The Discussion ID and Comment ID are stored as an object relationship for duplicate prevention. **Duplicate prevention:** Each synced message stores a relationship of `-`. If the same message has already been synced, it won't be sent again. --- ## Inbound: Smartsheet comments → FN messages When a comment is added to a discussion on a linked row: 1. The webhook fires with a `comment` event type. 2. The connector fetches the comment details from the Smartsheet API. 3. It fetches the parent discussion to determine which row the comment belongs to. 4. If the discussion's `parentType` is `ROW`, the connector maps the `-` to find the linked work order. 5. The comment text is posted as a message on the work order. > [INFO] Only comments on **row-level discussions** are synced. Sheet-level discussions (not attached to a specific row) are ignored by the connector. --- ## Troubleshooting - Verify the `messages` service flag is enabled - Confirm the work order has a valid linked external object ID (`-`) - Check that the message text is not empty - Verify the Access Token has permission to create discussions on the sheet - Verify your webhook `events` includes comment events (`*.*` or explicitly `comment.added`) - Confirm the comment was added to a **row-level** discussion (not a sheet-level discussion) - Check that the row is linked to a Field Nation work order - Verify the webhook is still `ENABLED` (check via API) - The connector tracks synced messages via object relationships — if relationships are cleared, duplicates can occur - Verify the object relationship record exists for previously synced messages - Do not manually re-sync rows that already have active message sync --- ## Summary | Configuration | Outbound (FN → Smartsheet) | Inbound (Smartsheet → FN) | | --- | --- | --- | | Messages **OFF** | No sync | No sync | | Messages **ON**, `private` | New Discussion per message (internal only) | Comment → internal message on work order | | Messages **ON**, `public` | New Discussion per message (visible to providers) | Comment → message on work order (type determined by work order status) | --- ## Resources (5) ### Environments URL: /docs/resources/environments ## Environment Overview ## Sandbox Environment A complete test environment that mirrors production functionality without affecting live data or operations. ### Access URLs ### Purpose - **Development** - Build and iterate on your integration - **Testing** - Test all functionality without risk - **Training** - Learn the platform and API - **Staging** - Final validation before production ### Key Characteristics - ✅ Identical features to production - ✅ Separate database (no live data) - ✅ Test work orders and providers - ✅ No real payments processed - ✅ Safe for experimentation - ✅ API credentials separate from production > [INFO] **Access Required**: Request sandbox access through a [support case](https://app.fieldnation.com/support-cases) with your Company ID and integration requirements. ### Webhook IP Addresses (Sandbox) Whitelist these IPs if your webhook endpoint uses IP filtering: ``` 18.215.51.196 3.223.100.250 44.199.193.222 ``` [Security Guide →](/docs/webhooks/guides/security) ## Production Environment The live Field Nation platform where real work orders, payments, and operations occur. ### Access URLs ### Purpose - **Live Operations** - Real work orders and payments - **Customer Facing** - Actual business operations - **Performance Critical** - Must be reliable and tested ### Key Characteristics - ⚠️ Real data and operations - ⚠️ Actual payments processed - ⚠️ Affects live work orders - ⚠️ Requires thorough testing first - ⚠️ Separate API credentials required - ⚠️ Change control recommended > **Production Access**: Requires approval and thorough testing in sandbox first. Contact Field Nation support to request production API credentials. ### Webhook IP Addresses (Production) Contact Field Nation support for production webhook IP addresses to whitelist. --- ## Environment Comparison | Feature | Sandbox | Production | |---------|---------|------------| | **Purpose** | Development & Testing | Live Operations | | **Data** | Test data only | Real customer data | | **Payments** | No real payments | Real payments processed | | **Work Orders** | Test work orders | Live work orders | | **API Credentials** | Sandbox-specific | Production-specific | | **Availability** | On request | Requires approval | | **Error Impact** | No real impact | Affects operations | | **Testing** | Encouraged | After sandbox validation | --- ## Getting Access ### Sandbox Access ### Submit Support Case Submit a [support case](https://app.fieldnation.com/support-cases) with: ``` Subject: Sandbox Access Request Company ID: [Your Company ID] Company Name: [Your Company Name] Integration Type: [REST API / Webhooks / Connector] Purpose: [Development and testing of integration] Technical Contact: Name: [Name] Email: [Email] Phone: [Phone] ``` ### Receive Credentials Field Nation team will: - Provision sandbox account - Provide access credentials - Share sandbox Company ID ### Begin Development Log in to sandbox and start building: - Create test work orders - Configure webhooks - Test API integrations --- ### Production Access ### Complete Sandbox Testing Ensure your integration: - ✅ Passes all test scenarios - ✅ Handles errors gracefully - ✅ Implements proper security - ✅ Meets performance requirements ### Submit Production Request Submit a [support case](https://app.fieldnation.com/support-cases): ``` Subject: Production API Access Request Company ID: [Your Production Company ID] Company Name: [Your Company Name] Integration Type: [REST API / Webhooks / Connector] Sandbox Testing Complete: - Testing Duration: [X weeks/months] - Test Scenarios: [List key scenarios tested] - Issues Resolved: [Any issues found and fixed] Production Use Case: [Describe your production integration] Expected Volume: - Work Orders per day: [Estimate] - API Requests per day: [Estimate] - Webhooks per day: [Estimate] ``` ### Receive Approval Field Nation will: - Review your request - Verify sandbox testing - Provide production credentials - Assign support contact ### Deploy to Production Follow deployment best practices: - Use environment variables for credentials - Monitor closely after deployment - Have rollback plan ready --- ## Testing Strategy ### Development Phase (Sandbox) **Goals:** Build core functionality ```mermaid graph LR A[Local Dev] --> B[ngrok Tunnel] B --> C[Sandbox Webhooks] C --> D[Test Events] D --> E[Verify Handling] ``` **Best Practices:** - Use ngrok or localtunnel for local testing - Log all requests and responses - Test individual endpoints first - Verify signature validation [Testing Guide →](/docs/webhooks/guides/testing) --- ### Integration Testing (Sandbox) **Goals:** Validate end-to-end flows ### Create Test Scenarios Define realistic workflows: - Work order creation and publishing - Provider assignment and acceptance - Work completion and approval - Payment processing ### Execute Test Cases Run comprehensive tests: - Happy path scenarios - Error handling - Edge cases - Data validation ### Monitor Results Track integration health: - Webhook delivery success rate - API response times - Error rates and types - Data accuracy ### Iterate and Improve Based on results: - Fix identified issues - Optimize performance - Enhance error handling - Document learnings --- ### Staging Phase (Sandbox) **Goals:** Production-like validation **Staging Checklist:** - ☐ All features implemented and tested - ☐ Error handling comprehensive - ☐ Security measures in place - ☐ Performance meets requirements - ☐ Monitoring and alerts configured - ☐ Documentation complete - ☐ Team trained on operations - ☐ Rollback procedure defined --- ### Production Deployment **Goals:** Safe, monitored launch 1. **Pre-Deployment** ```bash # Verify environment variables echo $FN_CLIENT_ID echo $FN_CLIENT_SECRET echo $FN_BASE_URL # Test connectivity curl https://api.fieldnation.com/health ``` 2. **Deployment** - Deploy webhook endpoint - Create production webhooks - Enable API integration - Monitor initial traffic 3. **Post-Deployment** - Run smoke tests - Verify webhook deliveries - Check error logs - Monitor metrics **Critical Metrics:** ```javascript // Track these in production const metrics = { // Webhook health webhookSuccessRate: '>99%', webhookLatency: '<2s p95', deliveryFailures: '<1%', // API health apiErrorRate: '<1%', apiLatency: '<500ms p95', // Business metrics workOrdersCreated: 'track daily', syncFailures: 'alert immediately' }; ``` **Set Up Alerts:** - Webhook delivery failures - High error rates - Slow response times - Authentication failures [Monitoring Guide →](/docs/webhooks/guides/monitoring) **If Issues Arise:** 1. **Immediate Actions** ```bash # Deactivate webhook curl -X PUT "https://api.fieldnation.com/api/v1/webhooks/wh_prod?access_token=$TOKEN" \ -d '{"status": "inactive"}' # Stop API calls # Revert to previous version ``` 2. **Assess Impact** - How many work orders affected? - Any data loss or corruption? - Customer impact level? 3. **Fix and Redeploy** - Identify root cause - Fix in sandbox - Test thoroughly - Redeploy with monitoring --- ## Migration: Sandbox → Production ### Code Changes ```javascript // ❌ Don't hardcode environments const baseUrl = 'https://api-sandbox.fndev.net'; // ✅ Use environment variables const baseUrl = process.env.FN_BASE_URL; // ❌ Don't commit credentials const clientId = 'sandbox_client_id'; // ✅ Use environment variables const clientId = process.env.FN_CLIENT_ID; ``` ### Configuration Files ```properties title=".env.sandbox" FN_BASE_URL=https://api-sandbox.fndev.net FN_CLIENT_ID=sandbox_client_id FN_CLIENT_SECRET=sandbox_secret FN_WEBHOOK_SECRET=sandbox_webhook_secret ``` ```properties title=".env.production" FN_BASE_URL=https://api.fieldnation.com FN_CLIENT_ID=prod_client_id FN_CLIENT_SECRET=prod_secret FN_WEBHOOK_SECRET=prod_webhook_secret ``` ### Deployment Checklist - ☐ Environment variables configured - ☐ Production credentials obtained - ☐ Webhook endpoints updated - ☐ IP whitelisting configured (if applicable) - ☐ Monitoring and alerts active - ☐ Error handling tested - ☐ Team notified of deployment - ☐ Rollback procedure ready --- ## Best Practices ### Do's ✅ - **Always test in sandbox first** - Use separate credentials for each environment - Implement environment-aware configuration - Monitor production closely after deployment - Keep sandbox and production code in sync - Document environment-specific settings - Test error scenarios in sandbox - Have a rollback plan ready ### Don'ts ❌ - **Never test in production** - Don't use sandbox credentials in production - Don't skip sandbox testing phase - Don't deploy without monitoring - Don't mix sandbox and production data - Don't hardcode environment URLs - Don't deploy during peak hours - Don't skip error handling --- ## Troubleshooting ### Wrong Environment **Symptom**: API calls work in sandbox but fail in production **Check:** ```javascript console.log('Base URL:', process.env.FN_BASE_URL); console.log('Client ID:', process.env.FN_CLIENT_ID.substring(0, 10) + '...'); // Verify you're calling correct environment if (process.env.FN_BASE_URL.includes('sandbox')) { console.log('✅ Using SANDBOX'); } else { console.log('✅ Using PRODUCTION'); } ``` ### Mixed Credentials **Symptom**: Authentication fails unexpectedly **Solution**: Verify credentials match environment ```bash # Check which environment credentials are for curl -X POST https://api-sandbox.fndev.net/authentication/api/oauth/token \ -d "grant_type=client_credentials" \ -d "client_id=$FN_CLIENT_ID" \ -d "client_secret=$FN_CLIENT_SECRET" ``` --- --- ### Error codes URL: /docs/resources/error-codes ## HTTP Status Codes ### Success Codes (2xx) **When you see these:** Your request was processed successfully. No action needed. --- ### Client Error Codes (4xx) These indicate problems with your request that need to be fixed before retrying. #### 400 Bad Request **Meaning:** The request is malformed or contains invalid parameters. **Common Causes:** - Invalid JSON syntax - Missing required fields - Invalid data types (string instead of number) - Malformed URLs or query parameters **Example Error Response:** ```json { "metadata": { "timestamp": "2026-01-15T12:00:00Z", "path": "/api/v1/webhooks" }, "errors": [ { "code": 400, "message": "Invalid request: 'url' must be a valid HTTPS URL" } ], "result": {} } ``` **Solution:** - Validate JSON structure before sending - Check all required fields are present - Verify data types match API specification - Review OpenAPI documentation for correct format --- #### 401 Unauthorized **Meaning:** Authentication failed or access token is invalid/expired. **Common Causes:** - Missing `Authorization` header - Expired access token (tokens expire after 1 hour) - Invalid client credentials - Incorrect token format **Example Error Response:** ```json { "metadata": { "timestamp": "2026-01-15T12:00:00Z" }, "errors": [ { "code": 401, "message": "Invalid or expired access token" } ], "result": {} } ``` **Solution:** - Generate a new access token using OAuth 2.0 - Ensure `?access_token={token}` query parameter is included in the URL - Implement token refresh logic before expiry - Verify client credentials are correct --- #### 403 Forbidden **Meaning:** Authentication succeeded but you don't have permission for this resource. **Common Causes:** - Insufficient permissions for the requested operation - API user lacks required role or access level - Attempting to access another company's resources **Solution:** - Verify your API user has appropriate permissions - Contact Field Nation support to review access levels - Ensure you're accessing resources within your company --- #### 404 Not Found **Meaning:** The requested resource doesn't exist. **Common Causes:** - Incorrect webhook ID, work order ID, or other identifier - Resource was deleted - Typo in endpoint URL - Using wrong environment (Sandbox vs Production) **Example:** ```bash # Wrong webhook ID GET /api/v1/webhooks/wh_invalid # Returns 404 # Correct GET /api/v1/webhooks/wh_abc123def456 ``` **Solution:** - Verify resource IDs are correct - Check resource still exists - Review endpoint URL for typos - Confirm you're using the correct environment --- #### 422 Unprocessable Entity **Meaning:** Request is well-formed but contains semantic errors or fails validation. **Common Causes:** - Invalid field values (e.g., invalid email format) - Business rule violations - Conflicting data - Unsupported event names **Example Error Response:** ```json { "metadata": { "timestamp": "2026-01-15T12:00:00Z" }, "errors": [ { "code": 422, "message": "Validation failed: 'events' must contain at least one valid event name" } ], "result": {} } ``` **Solution:** - Review validation error messages - Check field formats match requirements - Verify all referenced resources exist - Consult API documentation for field constraints --- #### 429 Too Many Requests **Meaning:** You've exceeded the API rate limit. **Common Causes:** - Making too many requests too quickly - Not respecting rate limit headers - Burst traffic exceeding limits **Example Error Response:** ```json { "metadata": { "timestamp": "2026-01-15T12:00:00Z" }, "errors": [ { "code": 429, "message": "Rate limit exceeded. Retry after 60 seconds." } ], "result": {} } ``` **Response Headers:** ```http X-RateLimit-Limit: 100 X-RateLimit-Remaining: 0 X-RateLimit-Reset: 1642252800 ``` **Solution:** - Implement exponential backoff for retries - Check `X-RateLimit-*` headers to track usage - Wait until `X-RateLimit-Reset` timestamp before retrying - Reduce request frequency or implement request queuing --- ### Server Error Codes (5xx) These indicate problems on Field Nation's side. Implement retry logic for these errors. #### 500 Internal Server Error **Meaning:** An unexpected error occurred on Field Nation's servers. **Common Causes:** - Temporary server issues - Unhandled edge cases - Database connectivity problems **Solution:** - Retry the request after a short delay - Implement exponential backoff (wait 1s, 2s, 4s, 8s...) - If error persists, contact Field Nation support - Include request details in support case --- #### 502 Bad Gateway **Meaning:** Field Nation's gateway received an invalid response from upstream server. **Common Causes:** - Temporary network issues - Service deployment in progress - Load balancer problems **Solution:** - Retry after a few seconds - Check [status.fieldnation.com](https://status.fieldnation.com) for incidents - If persistent, contact support --- #### 503 Service Unavailable **Meaning:** Service is temporarily unavailable, usually due to maintenance or overload. **Common Causes:** - Scheduled maintenance - System overload - Service restart **Solution:** - Check `Retry-After` header if present - Wait and retry after a delay - Monitor [status.fieldnation.com](https://status.fieldnation.com) - If extended, contact support --- #### 504 Gateway Timeout **Meaning:** Field Nation's gateway didn't receive a response in time. **Common Causes:** - Large payload processing - Slow database queries - Network latency **Solution:** - Retry the request - If creating/updating large resources, break into smaller operations - Contact support if consistently occurring --- ## Webhook-Specific Errors ### Delivery Failures | Error | Cause | Retry? | Solution | |-------|-------|--------|----------| | **Connection Refused** | Endpoint server not running | Yes | Start your server, verify port, check firewall | | **DNS Resolution Failed** | Domain doesn't resolve | No | Fix DNS configuration, wait for propagation | | **SSL Certificate Error** | Invalid/expired certificate | No | Renew certificate, use valid CA-signed cert | | **Connection Timeout** | Endpoint too slow to respond | Yes | Optimize response time, use async processing | | **Signature Verification Failed** | Wrong secret or body modified | No | Use correct secret, verify with raw body | [Complete webhook troubleshooting →](/docs/webhooks/troubleshooting/delivery-failures) --- ## Integration-Specific Errors ### Connector Errors **Authentication Failure:** ``` Error: OAuth token expired or invalid ``` **Solution:** Refresh connector authentication in Integration Broker settings. **Field Mapping Error:** ``` Error: Required field 'external_id' is missing ``` **Solution:** Update field mappings to include all required fields. **Data Type Mismatch:** ``` Error: Cannot convert 'text' to number for field 'priority' ``` **Solution:** Add data type conversion in field mapping or JSONNET transform. --- ## Error Handling Best Practices ### Implement Retry Logic ```javascript async function apiRequestWithRetry(url, options, maxRetries = 3) { for (let attempt = 1; attempt <= maxRetries; attempt++) { try { const response = await fetch(url, options); // Success if (response.ok) { return await response.json(); } // Client errors - don't retry if (response.status >= 400 && response.status < 500) { const error = await response.json(); throw new Error(`Client error: ${JSON.stringify(error.errors)}`); } // Server errors - retry with backoff if (response.status >= 500) { if (attempt === maxRetries) { throw new Error(`Max retries exceeded: ${response.status}`); } const delay = Math.pow(2, attempt) * 1000; // Exponential backoff console.log(`Attempt ${attempt} failed, retrying in ${delay}ms...`); await sleep(delay); continue; } } catch (error) { if (attempt === maxRetries) throw error; const delay = Math.pow(2, attempt) * 1000; await sleep(delay); } } } ``` ### Log Errors with Context ```javascript function logError(error, context) { console.error('API Error:', { timestamp: new Date().toISOString(), error: error.message, stack: error.stack, statusCode: error.statusCode, requestUrl: context.url, requestMethod: context.method, requestBody: context.body, responseBody: context.response }); } ``` ### Handle Different Error Types ```javascript try { const response = await createWebhook(config); } catch (error) { if (error.statusCode === 400) { // Validation error - fix request console.error('Invalid request:', error.message); } else if (error.statusCode === 401) { // Auth error - refresh token await refreshAccessToken(); return await createWebhook(config); } else if (error.statusCode === 429) { // Rate limit - wait and retry await sleep(error.retryAfter * 1000); return await createWebhook(config); } else if (error.statusCode >= 500) { // Server error - retry with backoff return await retryWithBackoff(() => createWebhook(config)); } } ``` --- ## Debugging Tools ### Check API Response ```bash # Verbose curl output curl -X GET "https://api-sandbox.fndev.net/api/v1/webhooks?access_token=YOUR_TOKEN" \ -v # Save response headers curl -X GET "https://api-sandbox.fndev.net/api/v1/webhooks?access_token=YOUR_TOKEN" \ -D headers.txt # Follow redirects curl -X GET "https://api-sandbox.fndev.net/api/v1/webhooks?access_token=YOUR_TOKEN" \ -L ``` ### Validate JSON Payload ```bash # Validate JSON syntax cat payload.json | jq . # Pretty print cat payload.json | jq '.' # Extract specific field cat payload.json | jq '.errors[0].message' ``` --- ## Getting Help If you encounter errors not covered here: 1. **Check System Status**: [status.fieldnation.com](https://status.fieldnation.com) 2. **Review Documentation**: Search for specific error messages 3. **Contact Support**: Submit a case with error details [Support resources →](/docs/resources/support) --- --- ### Faq URL: /docs/resources/faq ## Account & Access You need an active **Buyer Account** and an **Integration Contract**. See the [Prerequisites](/docs/getting-started/prerequisites) checklist for full details on: * Access requirements * Credentials (`client_id`, `client_secret`) * Company ID location **No.** Currently, the API is available only to **Buyers** (companies posting work). If you are a Service Company partner, contact your Field Nation representative to discuss specific partnership integration options. **User Interface:** [https://app-sandbox.fndev.net](https://app-sandbox.fndev.net) **API Endpoint:** `https://api-sandbox.fndev.net` *Note: Sandbox data does not sync with Production.* ## API & Webhooks **Use Both.** * **REST API**: For *outbound* actions (Creating work orders, assigning providers, updating custom fields). * **Webhooks**: For *inbound* updates (Receiving real-time status changes, deliverables, and messages). [Quick Start Strategy →](/docs/getting-started/quick-start) We use **ISO 8601** format with timezone offsets. * Example: `2023-10-27T14:30:00-05:00` Always parse the offset to ensure correct UTC conversion. Standard limits are **10 requests per second** per token. * Burst handling is available for short durations. * Webhooks have no receiving limit but may be queued during high volume. Yes. Use a tunneling service like **ngrok** to expose your localhost port to the internet. 1. Start ngrok: `ngrok http 3000` 2. Register the HTTPS URL in the [Integration UI](https://app-sandbox.fndev.net/integrations). ## Pre-Built Integrations We have "No-Code" connectors for major platforms including: * **CRM/Ticketing**: Salesforce, ServiceNow, Freshdesk, Zendesk. * **PSA**: ConnectWise Manage, Autotask. * **Project Management**: Smartsheet, Quickbase. [View All Connectors →](/docs/connectors/introduction) **Yes.** The Integration Broker allows: * **Direct Mapping**: 1-to-1 field sync. * **Static Values**: Hardcoded defaults. * **Logic (JSONNET)**: Conditional logic (e.g., "If Status is X, set Priority to Y"). ## Help & Resources **Submit a Case**: [Support Portal](https://app.fieldnation.com/support-cases) * Select "Integration" as the category. * Include your **Company ID** and **Environment** (Sandbox/Prod). **Urgent Issues**: Call +1 877-573-4353. It is in your **Company Profile** URL. `https://app.fieldnation.com/company/profile/{ID}` [See Screenshots →](/docs/getting-started/prerequisites#3-company-id) --- ### Glossary URL: /docs/resources/glossary - **API (Application Programming Interface)** — Protocols enabling software to talk to each other. Field Nation exposes REST API v2. - **Access Token** — OAuth2 client-credentials token; expires in 1 hour. - **Assignment** — Connecting a provider to a work order (marketplace, routing, or direct). - **Autotask** — PSA platform with a pre-built Field Nation connector. - **Base URL** — Sandbox: `https://api-sandbox.fndev.net`; production available from Field Nation. - **Buyer** — Posts work orders; has API access. - **Broker (Integration Broker)** — Middleware handling auth, transformation, queuing, retries. - **Client ID / Client Secret** — OAuth2 credentials; keep secrets out of source control. - **Company ID** — Org identifier (e.g., `/company/profile/123`). - **Connector / Out-of-the-Box Connector** — Pre-built integration (Salesforce, ServiceNow, Autotask, etc.). - **Custom Field** — Buyer-defined fields; can sync across systems. - **ConnectWise** — Business management platform with a connector. - **Dead Letter Queue (DLQ)** — Holds webhook deliveries after retries exhausted. - **Delivery Log** — Record of webhook attempts (request/response, status, timestamps). - **Deliverable** — Proof of completion (photos, docs, checklists). - **Event / Event ID** — Webhook-triggering occurrence; use IDs for idempotency. - **Exponential Backoff** — Retry delays double each attempt (10s, 20s, 40s, ...). - **Field Mapping** — Defines how fields correspond across systems. - **Field Nation Platform** — Buyer UI (`app.fieldnation.com`, sandbox `app-sandbox.fndev.net`). - **Freshdesk** — Support platform with a connector. - **GMT** — Timezone reference for UTC; timestamps use UTC with offsets. - **HMAC-SHA256** — Webhook signature algorithm. - **HTTP Status Code** — e.g., 200 success, 401 unauthorized, 404 not found, 500 server error. - **Idempotency** — Multiple deliveries, same result; rely on event IDs. - **Integration Broker** — See Broker. - **IP Whitelisting** — Restrict webhook ingress to Field Nation IP ranges. - **ISO 8601** — Timestamp format with timezone offsets. - **JSONNET** — Templating language used in connectors for transformations. - **Legacy Field** — Deprecated webhook mapping approach; prefer custom headers. - **Marketplace** — Where work orders are posted and providers engage. - **Message Queue** — Redis-backed queuing and retries for webhooks. - **Middleware** — Integration layer for routing, transformation, error handling. - **NetSuite** — ERP with a Field Nation connector. - **ngrok** — Tunneling tool for local webhook testing. - **OAuth 2.0** — Auth framework; client credentials flow for access tokens. - **OpenAPI Specification** — API description; Field Nation ships REST v2 & Webhooks v3 specs. - **Payload** — Body of API or webhook message. - **Production** — Live environment; special credentials required. - **Provider** — Contractor; no API access. - **Quickbase** — Low-code platform with a connector. - **Rate Limit** — Request quotas for fairness/stability. - **Redis** — Used for webhook queuing/caching. - **REST** — Architectural style; REST API v2 follows REST principles. - **Retry Logic** — Automatic retries with backoff. - **Routing** — Auto-matching providers to work orders. - **Salesforce** — CRM with Service Cloud connector. - **Sandbox** — Test environment (`https://app-sandbox.fndev.net`). - **Schedule (Service Window)** — When work should be performed. - **Scope of Work (SOW)** — Detailed work description. - **Secret (Webhook Secret)** — Key used for HMAC signatures. - **Service Company** — See Provider. - **ServiceNow** — ITSM connector. - **Signature (Webhook Signature)** — HMAC-SHA256 hash in `x-fn-signature`. - **Smartsheet** — Work management connector. - **Status (Work Order Status)** — Lifecycle states (draft, published, assigned, done, approved, paid). - **Status Page** — [status.fieldnation.com](https://status.fieldnation.com). - **Template** — Pre-configured work order blueprint. - **Timestamp** — ISO 8601 with timezone offset. - **Token** — See Access Token. - **UTC** — Global time standard; used with offsets. - **Vetting** — Provider evaluation (background, insurance, performance). - **Webhook** — Event callback; Webhooks v3 used. - **Work Order / Work Order ID** — Job record and its identifier. - **Zendesk** — Customer service platform with connector. ## Additional Resources Begin your integration journey Frequently asked questions Get help with your integration --- ### Support URL: /docs/resources/support ## Support Channels ### Submit a Support Case The primary way to get integration support is through the Field Nation Support Portal. **Support Portal**: [app.fieldnation.com/support-cases](https://app.fieldnation.com/support-cases) **When to submit a case:** - Request API credentials or sandbox access - Report technical issues or errors - Get help with integration configuration - Request feature enhancements - Report security concerns > [INFO] **Response Time**: Support cases are typically responded to within 1 business day for standard issues. Critical issues receive faster attention. --- ### Phone Support For urgent issues or immediate assistance: **Field Nation Support**: +1 877-573-4353 **Available**: 24/7, including weekends and holidays - **Monday - Friday**: Full support capacity - **Weekends & Holidays**: Limited support capacity **Best for:** - Critical production issues - Time-sensitive problems - Complex technical discussions - Escalation of existing cases > [INFO] **24/7 Availability**: Field Nation support is available around the clock. For the fastest resolution, submitting a support case with detailed error logs and screenshots is recommended, especially for non-urgent integration-specific technical issues. --- ## What to Include in Support Requests ### Essential Information Always include these details when submitting a support case: ### Environment Details Specify which environment you're working with: - **Sandbox**: `https://app-sandbox.fndev.net` or `https://api-sandbox.fndev.net` - **Production**: `https://app.fieldnation.com` or `https://api.fieldnation.com` ### Company Information - **Company ID**: Found in Company Settings > Company Profile - **Company Name**: Your organization name in Field Nation ### Integration Type Specify your integration approach: - REST API v2 - Webhooks v3 - Pre-built Connector (specify which: Salesforce, ServiceNow, etc.) - Custom integration ### Error Details Include comprehensive error information: - Full error messages - HTTP status codes - Timestamps (in UTC or with timezone) - Request/response payloads (remove sensitive data) - Relevant log excerpts ### Steps to Reproduce Provide clear steps to recreate the issue: 1. What you were trying to do 2. Actions you took 3. What you expected to happen 4. What actually happened ### Impact Assessment Help us prioritize: - How many users/work orders affected? - Is this blocking production operations? - Is there a workaround available? --- ## Support Request Templates ### API Credential Request ``` Subject: API Credential Request - [Sandbox/Production] Environment: [Sandbox/Production] Company ID: [Your Company ID] Company Name: [Your Company Name] Integration Type: [REST API / Webhooks / Connector Name] Purpose: [Brief description of your integration goals] Requested Access: - [ ] REST API v2 access - [ ] Webhooks v3 access - [ ] Specific connector access Technical Contact: Name: [Name] Email: [Email] Phone: [Phone] ``` --- ### Technical Issue Report ``` Subject: [Integration Issue] - Brief Description Environment: [Sandbox/Production] Company ID: [Your Company ID] Integration Type: [REST API / Webhooks / Connector] Issue Description: [Clear description of the problem] Error Details: - Error Message: [Full error message] - Status Code: [HTTP status code if applicable] - Timestamp: [When the error occurred] - Request ID: [If available] Steps to Reproduce: 1. [Step 1] 2. [Step 2] 3. [Step 3] Expected Behavior: [What should happen] Actual Behavior: [What actually happens] Impact: [How this affects your operations] Logs/Screenshots: [Attach relevant logs or screenshots] ``` --- ### Webhook Delivery Failure ``` Subject: Webhook Delivery Failures - [Webhook ID] Environment: [Sandbox/Production] Company ID: [Your Company ID] Webhook ID: [wh_xxxxx] Endpoint URL: [Your webhook URL] Issue: Webhooks are failing to deliver with [error description] Delivery Log Details: - Delivery ID: [del_xxxxx] - Event Name: [event name] - Delivery Status: [HTTP status] - Error Message: [error from delivery log] - Timestamp: [when failures started] Endpoint Status: - [ ] Endpoint is publicly accessible - [ ] SSL certificate is valid - [ ] Firewall allows Field Nation IPs - [ ] Endpoint responds within 30 seconds Recent Changes: [Any recent changes to your endpoint or infrastructure] ``` --- ## Self-Service Resources ### Documentation Comprehensive documentation for all integration approaches: - **Getting Started**: [Quick Start](/docs/getting-started/quick-start) - **REST API**: Available in REST API section (to be built) - **Webhooks**: [Introduction](/docs/webhooks/introduction) - **Connectors**: Available in Connectors section (to be built) ### Status Page Check current system status and incident history: **Status Page**: [status.fieldnation.com](https://status.fieldnation.com) **What's available:** - Real-time system status (Web App, Mobile App, API, Connectors) - Active incidents and resolution progress - Scheduled maintenance notifications - Past incident history - Subscribe to email/SMS notifications > [INFO] Before submitting a support case, check the status page to see if there's a known issue affecting your integration. --- ### FAQ Quick answers to common questions: [View FAQ →](/docs/resources/faq) **Popular topics:** - Getting started and account setup - API credentials and authentication - Webhook configuration - Integration troubleshooting - Data synchronization --- ### Glossary Understand Field Nation terminology: [View Glossary →](/docs/resources/glossary) Comprehensive A-Z reference for platform concepts, technical terms, and integration terminology. --- ## Emergency Escalation ### Production-Critical Issues If you're experiencing a production-critical issue: 1. **Call Support Immediately**: +1 877-573-4353 2. **Mark Case as Critical**: When submitting a case, indicate severity level 3. **Provide Impact Details**: Number of affected work orders, users, or transactions 4. **Include Workaround Status**: Whether operations are completely blocked ### Security Issues For security vulnerabilities or concerns: 1. **Do not post in public forums** 2. **Submit a confidential support case** marked "Security Issue" 3. **Include detailed vulnerability description** 4. **Follow responsible disclosure practices** --- ## Support Response Times | Severity | Response Time | Example | |----------|---------------|---------| | **Critical** | 2-4 hours | Production system down, data loss, security breach | | **High** | 4-8 hours | Major feature not working, significant user impact | | **Medium** | 1 business day | Non-critical bugs, minor feature issues | | **Low** | 2-3 business days | Enhancement requests, documentation questions | > [INFO] **Note**: Response times are for initial response. Resolution time varies based on issue complexity. --- ## Additional Resources ### Community & Feedback **Feature Requests**: Submit through support portal with detailed use case **Feedback**: Share your integration experience through support cases ### Training & Onboarding Contact your Field Nation representative for: - Integration onboarding sessions - Technical training - Best practices consultation - Architecture review --- ## Tips for Effective Support Requests ### Do's ✅ - Provide complete error messages and logs - Include timestamps in UTC or with timezone - Attach screenshots when helpful - Specify exact steps to reproduce - Remove sensitive data before sharing logs - Follow up with additional information if requested ### Don'ts ❌ - Don't say "it doesn't work" without details - Don't share API credentials in support cases - Don't submit duplicate cases for the same issue - Don't expect instant resolution for complex issues - Don't wait until production to test and report issues --- ## Contact Information Quick Reference | Need | Channel | Link | |------|---------|------| | **Submit Case** | Support Portal | [app.fieldnation.com/support-cases](https://app.fieldnation.com/support-cases) | | **Phone Support** | Phone | +1 877-573-4353 | | **System Status** | Status Page | [status.fieldnation.com](https://status.fieldnation.com) | | **Documentation** | Docs Site | [developers.fieldnation.com](https://developers.fieldnation.com) | --- --- ## REST Playground (156) ### Overview URL: /docs/rest-api-playground/2026-02-09/overview {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} --- ### Token POST URL: /docs/rest-api-playground/2026-02-09/authentication/token-post {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Exchange your client and user credentials for an access token. Use the returned `access_token` as a **query parameter** (`?access_token=TOKEN`) when calling the Client API — NOT as a Bearer header. Example: `GET /api/rest/v2/workorders?access_token=TOKEN` --- ### Expenses GET URL: /docs/rest-api-playground/2026-02-09/resources/configuration/company/expenses-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve all expense categories configured for your company. --- ### Incomplete reasons GET URL: /docs/rest-api-playground/2026-02-09/resources/configuration/company/incomplete-reasons-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve all incomplete reason options configured for your company. --- ### Managers GET URL: /docs/rest-api-playground/2026-02-09/resources/configuration/company/managers-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve all managers associated with your company. --- ### Networks GET URL: /docs/rest-api-playground/2026-02-09/resources/configuration/company/networks-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve all networks accessible by the specified company. --- ### Site revisit reasons GET URL: /docs/rest-api-playground/2026-02-09/resources/configuration/company/site-revisit-reasons-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve available reasons that providers and your company can select when entering revisit and job incomplete details. --- ### Tags GET URL: /docs/rest-api-playground/2026-02-09/resources/configuration/company/tags-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve all tags associated with your company. --- ### Tags POST URL: /docs/rest-api-playground/2026-02-09/resources/configuration/company/tags-post {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Create a new tag for your company. --- ### Templateid GET URL: /docs/rest-api-playground/2026-02-09/resources/configuration/templates/templateid-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve a single work order template by `template_id`, including its tasks and configuration. --- ### Templates GET URL: /docs/rest-api-playground/2026-02-09/resources/configuration/templates/templates-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve available work order templates that can be used to quickly create work orders with predefined fields and tasks. --- ### Service types GET URL: /docs/rest-api-playground/2026-02-09/resources/configuration/types-of-work/service-types-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve all service types associated with a specific type of work. --- ### Types of work GET URL: /docs/rest-api-playground/2026-02-09/resources/configuration/types-of-work/types-of-work-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve all available types of work and their associated service categories. --- ### Clientid GET URL: /docs/rest-api-playground/2026-02-09/resources/organization/clients/clientid-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve details for a single client company identified by `client_id`. --- ### Clients GET URL: /docs/rest-api-playground/2026-02-09/resources/organization/clients/clients-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve a paginated list of client companies with optional filters, lists, and column selection. --- ### Clients POST URL: /docs/rest-api-playground/2026-02-09/resources/organization/clients/clients-post {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Creates a new client for your company --- ### Locationid GET URL: /docs/rest-api-playground/2026-02-09/resources/organization/locations/locationid-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve details for a single saved location identified by `location_id`. --- ### Locations GET URL: /docs/rest-api-playground/2026-02-09/resources/organization/locations/locations-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Gets stored locations --- ### Locations POST URL: /docs/rest-api-playground/2026-02-09/resources/organization/locations/locations-post {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Add a location to company --- ### Dispatch settings GET URL: /docs/rest-api-playground/2026-02-09/resources/organization/projects/dispatch-settings-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Get Dispatch Settings --- ### Dispatch settings POST URL: /docs/rest-api-playground/2026-02-09/resources/organization/projects/dispatch-settings-post {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} This endpoint allows you to create smart dispatch settings for a project. --- ### Dispatch settings PUT URL: /docs/rest-api-playground/2026-02-09/resources/organization/projects/dispatch-settings-put {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} This endpoint allows you to update smart dispatch settings for a project. --- ### Projectid GET URL: /docs/rest-api-playground/2026-02-09/resources/organization/projects/projectid-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve the full details for a single project identified by `project_id`. --- ### Projects GET URL: /docs/rest-api-playground/2026-02-09/resources/organization/projects/projects-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve a paginated list of projects for your company with optional filters and column selection. --- ### Projects POST URL: /docs/rest-api-playground/2026-02-09/resources/organization/projects/projects-post {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Create a new project for your company, including core details and configuration for organizing work orders. --- ### Providerid GET URL: /docs/rest-api-playground/2026-02-09/resources/organization/projects/providerid-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} This endpoint retrieves all talent pool groups and their tiers that a provider is assigned to within a specific project --- ### Providerid PUT URL: /docs/rest-api-playground/2026-02-09/resources/organization/projects/providerid-put {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} This endpoint updates a provider's talent pool group and tier assignments for a specific project. It removes old tier assignments and adds new ones based on the provided data. The provider ID itself cannot be changed. --- ### Providers POST URL: /docs/rest-api-playground/2026-02-09/resources/organization/projects/providers-post {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} This endpoint allows you to add one or more providers to multiple talent pool groups with specific tier (talent pool) assignments. Each talent pool group can have multiple tier assignments, and the providers will be added to the specified tier within each talent pool group. --- ### Preferred provider groups GET URL: /docs/rest-api-playground/2026-02-09/resources/people/preferred-providers/preferred-provider-groups-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve all talent pool groups (preferred provider groups) available to your company. --- ### Preferredprovidergroupid GET URL: /docs/rest-api-playground/2026-02-09/resources/people/preferred-providers/preferredprovidergroupid-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve detailed information about a specific talent pool group by its ID. --- ### Select all GET URL: /docs/rest-api-playground/2026-02-09/resources/people/preferred-providers/select-all-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve all provider IDs associated with a specific preferred provider group. --- ### Userid POST URL: /docs/rest-api-playground/2026-02-09/resources/people/preferred-providers/userid-post {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Add a provider to a specific talent pool group. --- ### Users DELETE URL: /docs/rest-api-playground/2026-02-09/resources/people/preferred-providers/users-delete {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Remove one or more providers from a talent pool group. --- ### Users GET URL: /docs/rest-api-playground/2026-02-09/resources/people/preferred-providers/users-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve all providers (users) in a specific talent pool group. --- ### Qualificationid DELETE URL: /docs/rest-api-playground/2026-02-09/resources/people/qualifications/qualificationid-delete {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Delete Work Order Qualification --- ### Qualifications GET URL: /docs/rest-api-playground/2026-02-09/resources/people/qualifications/qualifications-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} List the qualification rules and outcomes for the work order. --- ### Qualifications POST URL: /docs/rest-api-playground/2026-02-09/resources/people/qualifications/qualifications-post {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Add or update qualification requirements for a work order. --- ### Qualifications PUT URL: /docs/rest-api-playground/2026-02-09/resources/people/qualifications/qualifications-put {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Update Work Order Qualification --- ### Providerid DELETE URL: /docs/rest-api-playground/2026-02-09/resources/people/service-territories/providerid-delete {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Removes a provider from a service territory. Either tierId or removeFromProject query parameter is required. If tierId is provided: Removes the provider from the specified talent pool within the group. If removeFromProject is true: Removes the provider from all service territories in the project. --- ### Providers GET URL: /docs/rest-api-playground/2026-02-09/resources/people/service-territories/providers-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Get a paginated list of providers in a specific service territory --- ### Service territories GET URL: /docs/rest-api-playground/2026-02-09/resources/people/service-territories/service-territories-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Get a paginated list of service territories --- ### Service territories POST URL: /docs/rest-api-playground/2026-02-09/resources/people/service-territories/service-territories-post {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Create a new service territory --- ### Serviceterritoryuuid DELETE URL: /docs/rest-api-playground/2026-02-09/resources/people/service-territories/serviceterritoryuuid-delete {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Delete a specific service territory by its ID --- ### Serviceterritoryuuid GET URL: /docs/rest-api-playground/2026-02-09/resources/people/service-territories/serviceterritoryuuid-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Get a specific service territory by its ID --- ### Serviceterritoryuuid PUT URL: /docs/rest-api-playground/2026-02-09/resources/people/service-territories/serviceterritoryuuid-put {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Update a specific service territory and its associated talent pools --- ### Groupuuid DELETE URL: /docs/rest-api-playground/2026-02-09/resources/people/talent-pool-groups/groupuuid-delete {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Delete a specific talent pool group and all its associated data. --- ### Groupuuid GET URL: /docs/rest-api-playground/2026-02-09/resources/people/talent-pool-groups/groupuuid-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve detailed information about a specific talent pool group by its UUID. --- ### Groupuuid PUT URL: /docs/rest-api-playground/2026-02-09/resources/people/talent-pool-groups/groupuuid-put {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Update a specific talent pool group and its associated talent pools. --- ### Providerid DELETE URL: /docs/rest-api-playground/2026-02-09/resources/people/talent-pool-groups/providerid-delete {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Remove a provider from a talent pool group. Either `tierId` or `removeFromProject` query parameter is required. If `tierId` is provided, removes the provider from the specified talent pool (tier) within the group. If `removeFromProject` is true, removes the provider from all talent pool groups in the project. --- ### Providers GET URL: /docs/rest-api-playground/2026-02-09/resources/people/talent-pool-groups/providers-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve a paginated list of providers in a specific talent pool group with optional sorting. --- ### Talent pool groups GET URL: /docs/rest-api-playground/2026-02-09/resources/people/talent-pool-groups/talent-pool-groups-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve a paginated list of talent pool groups with optional filtering and sorting. --- ### Talent pool groups POST URL: /docs/rest-api-playground/2026-02-09/resources/people/talent-pool-groups/talent-pool-groups-post {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Create a new talent pool group with associated talent pools. --- ### Attributes GET URL: /docs/rest-api-playground/2026-02-09/resources/people/talent-pools/attributes-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve a paginated list of attributes for a specific provider in a talent pool with optional sorting. --- ### Attributes POST URL: /docs/rest-api-playground/2026-02-09/resources/people/talent-pools/attributes-post {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Add new attributes for a specific provider in a talent pool. --- ### Attributes PUT URL: /docs/rest-api-playground/2026-02-09/resources/people/talent-pools/attributes-put {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Bulk update multiple attributes for a specific provider in a talent pool. --- ### Attributeuuid DELETE URL: /docs/rest-api-playground/2026-02-09/resources/people/talent-pools/attributeuuid-delete {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Delete a specific attribute for a provider in a talent pool. --- ### Attributeuuid PUT URL: /docs/rest-api-playground/2026-02-09/resources/people/talent-pools/attributeuuid-put {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Update the value of a specific attribute for a provider in a talent pool. --- ### Userid GET URL: /docs/rest-api-playground/2026-02-09/resources/people/users/userid-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve detailed information about a specific user by their ID. --- ### Lists GET URL: /docs/rest-api-playground/2026-02-09/work-orders/basics/core-operations/lists-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Return saved work order lists (e.g., tabs or saved searches) and their current counts, honoring the `list` and sticky parameters. --- ### Smart dispatch POST URL: /docs/rest-api-playground/2026-02-09/work-orders/basics/core-operations/smart-dispatch-post {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} This endpoint allows you to trigger smart dispatch process for one or more workorders. --- ### Workorderid DELETE URL: /docs/rest-api-playground/2026-02-09/work-orders/basics/core-operations/workorderid-delete {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Cancel and delete a work order. A `cancel_reason` is required to document the reason for deletion. --- ### Workorderid GET URL: /docs/rest-api-playground/2026-02-09/work-orders/basics/core-operations/workorderid-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Get full details for a specific work order. Use `columns` to control which sections are included in the response. --- ### Workorderid PUT URL: /docs/rest-api-playground/2026-02-09/work-orders/basics/core-operations/workorderid-put {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Update an existing work order. Only the provided fields will be updated. --- ### Workorders GET URL: /docs/rest-api-playground/2026-02-09/work-orders/basics/core-operations/workorders-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve a paginated list of work orders for your company. Use the extensive query filters (status, dates, location, pay, template, etc.) and `page`/`per_page` to control pagination. Use `columns`/`view` to adjust which fields are returned. --- ### Workorders POST URL: /docs/rest-api-playground/2026-02-09/work-orders/basics/core-operations/workorders-post {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Create a new work order. Provide the essential details for the job; the response returns the newly created work order with identifiers. --- ### Custom fields GET URL: /docs/rest-api-playground/2026-02-09/work-orders/basics/custom-fields/custom-fields-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve all custom fields available for work orders. --- ### Customfieldid GET URL: /docs/rest-api-playground/2026-02-09/work-orders/basics/custom-fields/customfieldid-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve a single custom field configuration/value by `custom_field_id`. --- ### Customfieldid PUT URL: /docs/rest-api-playground/2026-02-09/work-orders/basics/custom-fields/customfieldid-put {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Update a custom field value/configuration for the work order. --- ### Customfields GET URL: /docs/rest-api-playground/2026-02-09/work-orders/basics/custom-fields/customfields-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} List custom fields configured for this work order, including values and visibility. --- ### Tagid DELETE URL: /docs/rest-api-playground/2026-02-09/work-orders/basics/tags/tagid-delete {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Remove a tag from the work order by `tag_id`. --- ### Tags GET URL: /docs/rest-api-playground/2026-02-09/work-orders/basics/tags/tags-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} List tags assigned to the work order. --- ### Tags POST URL: /docs/rest-api-playground/2026-02-09/work-orders/basics/tags/tags-post {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Add a tag to the work order. --- ### Contactid DELETE URL: /docs/rest-api-playground/2026-02-09/work-orders/communication/contacts/contactid-delete {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Remove a contact from the work order by `contact_id`. --- ### Contactid PUT URL: /docs/rest-api-playground/2026-02-09/work-orders/communication/contacts/contactid-put {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Update a contact for the work order by `contact_id`. --- ### Contacts GET URL: /docs/rest-api-playground/2026-02-09/work-orders/communication/contacts/contacts-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} List contacts associated with the work order. --- ### Contacts POST URL: /docs/rest-api-playground/2026-02-09/work-orders/communication/contacts/contacts-post {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Add a new contact to the work order. --- ### Messageid POST URL: /docs/rest-api-playground/2026-02-09/work-orders/communication/messages/messageid-post {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Reply to an existing work order message by its `message_id`. Supports asynchronous processing via `async=true`. --- ### Messages GET URL: /docs/rest-api-playground/2026-02-09/work-orders/communication/messages/messages-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve all messages for a work order. Use the `archived` filter to include or exclude archived messages. --- ### Messages POST URL: /docs/rest-api-playground/2026-02-09/work-orders/communication/messages/messages-post {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Post a new message to a work order. Optionally set `async=true` to process the delivery asynchronously. --- ### Attachmentid DELETE URL: /docs/rest-api-playground/2026-02-09/work-orders/execution/attachments/attachmentid-delete {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Delete an attachment file from the specified folder. --- ### Attachmentid PUT URL: /docs/rest-api-playground/2026-02-09/work-orders/execution/attachments/attachmentid-put {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Update attachment metadata (for example, visibility or notes) within the specified folder. --- ### Attachments GET URL: /docs/rest-api-playground/2026-02-09/work-orders/execution/attachments/attachments-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} List attachment folders and files associated with the work order. --- ### Attachments POST URL: /docs/rest-api-playground/2026-02-09/work-orders/execution/attachments/attachments-post {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Create a new attachment folder to organize files for the work order. --- ### Folderid POST URL: /docs/rest-api-playground/2026-02-09/work-orders/execution/attachments/folderid-post {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Upload a file to the specified attachment folder using `multipart/form-data`. --- ### Problemid DELETE URL: /docs/rest-api-playground/2026-02-09/work-orders/execution/problems/problemid-delete {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Delete a problem record by `problem_id`. --- ### Problemid PUT URL: /docs/rest-api-playground/2026-02-09/work-orders/execution/problems/problemid-put {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Update an existing problem record by `problem_id`. --- ### Problems GET URL: /docs/rest-api-playground/2026-02-09/work-orders/execution/problems/problems-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} List reported problems for the work order, including types and statuses. --- ### Problems POST URL: /docs/rest-api-playground/2026-02-09/work-orders/execution/problems/problems-post {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Report a new problem for the work order. --- ### Signatureid DELETE URL: /docs/rest-api-playground/2026-02-09/work-orders/execution/signatures/signatureid-delete {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Delete a signature from the work order by its ID. --- ### Signatureid GET URL: /docs/rest-api-playground/2026-02-09/work-orders/execution/signatures/signatureid-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve a single signature by its ID for the given work order. --- ### Signatures GET URL: /docs/rest-api-playground/2026-02-09/work-orders/execution/signatures/signatures-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} List signatures captured for the work order with metadata and authorship. --- ### Signatures POST URL: /docs/rest-api-playground/2026-02-09/work-orders/execution/signatures/signatures-post {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Add a signature to the work order. Include the image data and metadata in the JSON payload. --- ### Alertid DELETE URL: /docs/rest-api-playground/2026-02-09/work-orders/execution/tasks/alertid-delete {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Delete a single alert for the specified task by `alert_id`. --- ### Alerts DELETE URL: /docs/rest-api-playground/2026-02-09/work-orders/execution/tasks/alerts-delete {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Clear all alerts for the specified task. --- ### Taskid DELETE URL: /docs/rest-api-playground/2026-02-09/work-orders/execution/tasks/taskid-delete {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Remove a task from the work order by `task_id`. --- ### Taskid GET URL: /docs/rest-api-playground/2026-02-09/work-orders/execution/tasks/taskid-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve the details for a single task by `task_id`. --- ### Taskid PUT URL: /docs/rest-api-playground/2026-02-09/work-orders/execution/tasks/taskid-put {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Update the details for a single task by `task_id`. --- ### Tasks GET URL: /docs/rest-api-playground/2026-02-09/work-orders/execution/tasks/tasks-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} List tasks defined for the work order with current status and metadata. --- ### Tasks POST URL: /docs/rest-api-playground/2026-02-09/work-orders/execution/tasks/tasks-post {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Create a new task within the work order. --- ### Tasks PUT URL: /docs/rest-api-playground/2026-02-09/work-orders/execution/tasks/tasks-put {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Bulk update multiple tasks for a work order in a single request. --- ### Timelogs GET URL: /docs/rest-api-playground/2026-02-09/work-orders/execution/time-logs/timelogs-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} List all time logs recorded against the work order, including check-in/out and verification details. --- ### Timelogs POST URL: /docs/rest-api-playground/2026-02-09/work-orders/execution/time-logs/timelogs-post {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Add a new time log (check-in or check-out) for the work order. --- ### Timelogs PUT URL: /docs/rest-api-playground/2026-02-09/work-orders/execution/time-logs/timelogs-put {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Bulk update time logs for a work order in a single request. --- ### Workorderhoursid DELETE URL: /docs/rest-api-playground/2026-02-09/work-orders/execution/time-logs/workorderhoursid-delete {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Delete a single time log entry from the work order. --- ### Workorderhoursid PUT URL: /docs/rest-api-playground/2026-02-09/work-orders/execution/time-logs/workorderhoursid-put {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Update a single time log entry for the work order. --- ### Bonuses GET URL: /docs/rest-api-playground/2026-02-09/work-orders/financials/bonuses/bonuses-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve all bonus options available for work orders. --- ### Bundle DELETE URL: /docs/rest-api-playground/2026-02-09/work-orders/financials/bundles/bundle-delete {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Delete the bundle associated with the specified work order. --- ### Bundle POST URL: /docs/rest-api-playground/2026-02-09/work-orders/financials/bundles/bundle-post {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Create a bundle that groups multiple work orders together for unified management and routing. --- ### Unbundle POST URL: /docs/rest-api-playground/2026-02-09/work-orders/financials/bundles/unbundle-post {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Remove work orders from a previously created bundle. --- ### Validate POST URL: /docs/rest-api-playground/2026-02-09/work-orders/financials/bundles/validate-post {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Validate that a list of work orders can be bundled together. Returns validation diagnostics without making changes. --- ### Bonuses GET URL: /docs/rest-api-playground/2026-02-09/work-orders/financials/finances/bonuses-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} List bonus entries that have been applied or are available for the work order. --- ### Bonusid POST URL: /docs/rest-api-playground/2026-02-09/work-orders/financials/finances/bonusid-post {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Create or apply a bonus by `bonus_id` to the work order. --- ### Discounts GET URL: /docs/rest-api-playground/2026-02-09/work-orders/financials/finances/discounts-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} List any discounts applied to the work order. --- ### Expenseid PUT URL: /docs/rest-api-playground/2026-02-09/work-orders/financials/finances/expenseid-put {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Update an existing expense entry for the work order. --- ### Expenses GET URL: /docs/rest-api-playground/2026-02-09/work-orders/financials/finances/expenses-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} List recorded expenses for the work order along with metadata and statuses. --- ### Expenses POST URL: /docs/rest-api-playground/2026-02-09/work-orders/financials/finances/expenses-post {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Create a new expense entry for the work order. --- ### Increaseid GET URL: /docs/rest-api-playground/2026-02-09/work-orders/financials/finances/increaseid-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve details for a specific pay increase on this work order. --- ### Increaseid PUT URL: /docs/rest-api-playground/2026-02-09/work-orders/financials/finances/increaseid-put {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Update a specific pay increase entry for the work order. --- ### Increases GET URL: /docs/rest-api-playground/2026-02-09/work-orders/financials/finances/increases-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} List pay increases applied to the work order. --- ### Pay GET URL: /docs/rest-api-playground/2026-02-09/work-orders/financials/finances/pay-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Get the full pay breakdown for a work order, including base, additional, fees, bonuses, expenses, penalties and totals. --- ### Pay PUT URL: /docs/rest-api-playground/2026-02-09/work-orders/financials/finances/pay-put {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Update the pay configuration for a work order. Only provided fields are changed. --- ### Penalties GET URL: /docs/rest-api-playground/2026-02-09/work-orders/financials/finances/penalties-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} List penalty entries associated with the work order. --- ### Penaltyid POST URL: /docs/rest-api-playground/2026-02-09/work-orders/financials/finances/penaltyid-post {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Create or apply a penalty by `penalty_id` to the work order. --- ### Penalties GET URL: /docs/rest-api-playground/2026-02-09/work-orders/financials/penalties/penalties-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve all penalty options available for work orders. --- ### Location GET URL: /docs/rest-api-playground/2026-02-09/work-orders/logistics/location/location-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve the current location for a work order, including saved location details when applicable. --- ### Location PUT URL: /docs/rest-api-playground/2026-02-09/work-orders/logistics/location/location-put {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Update the location associated with the work order (address, saved location, or coordinates). --- ### Shipmentid DELETE URL: /docs/rest-api-playground/2026-02-09/work-orders/logistics/shipments/shipmentid-delete {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Remove a shipment entry from the work order by `shipment_id`. --- ### Shipmentid PUT URL: /docs/rest-api-playground/2026-02-09/work-orders/logistics/shipments/shipmentid-put {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Update a shipment entry for the work order by `shipment_id`. --- ### Shipments GET URL: /docs/rest-api-playground/2026-02-09/work-orders/logistics/shipments/shipments-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} List shipments related to the work order, including direction, carrier and status. --- ### Shipments POST URL: /docs/rest-api-playground/2026-02-09/work-orders/logistics/shipments/shipments-post {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Create a shipment record for the work order (incoming or outgoing). --- ### Assignee DELETE URL: /docs/rest-api-playground/2026-02-09/work-orders/workflow/assignments/assignee-delete {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Remove the assigned provider (user) from a work order. --- ### Cancel POST URL: /docs/rest-api-playground/2026-02-09/work-orders/workflow/assignments/cancel-post {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Unassign and remove the currently assigned provider from the work order. --- ### Delay POST URL: /docs/rest-api-playground/2026-02-09/work-orders/workflow/assignments/delay-post {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Acknowledge a provider-reported delay on the work order. --- ### Providers GET URL: /docs/rest-api-playground/2026-02-09/work-orders/workflow/assignments/providers-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} List pending provider requests (e.g., route requests) for the work order. --- ### Milestones GET URL: /docs/rest-api-playground/2026-02-09/work-orders/workflow/milestones/milestones-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} List key lifecycle timestamps for the work order (created, published, routed, assigned, work done, approved, paid). --- ### Incident DELETE URL: /docs/rest-api-playground/2026-02-09/work-orders/workflow/revisits/incident-delete {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Remove the incident ID from the work order. --- ### Incident PUT URL: /docs/rest-api-playground/2026-02-09/work-orders/workflow/revisits/incident-put {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Update incident ID associated with the work order. --- ### Job status PUT URL: /docs/rest-api-playground/2026-02-09/work-orders/workflow/revisits/job-status-put {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Update the job status of a work order (e.g., scope of work complete or incomplete). --- ### Revisitid DELETE URL: /docs/rest-api-playground/2026-02-09/work-orders/workflow/revisits/revisitid-delete {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Delete a site revisit entry by `revisit_id`. --- ### Revisitid PUT URL: /docs/rest-api-playground/2026-02-09/work-orders/workflow/revisits/revisitid-put {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Update a site revisit entry by `revisit_id`. --- ### Site revisits GET URL: /docs/rest-api-playground/2026-02-09/work-orders/workflow/revisits/site-revisits-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} List site revisit entries for a work order, including counts and latest revisit details. --- ### Site revisits POST URL: /docs/rest-api-playground/2026-02-09/work-orders/workflow/revisits/site-revisits-post {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Create a site revisit record for the work order. --- ### Eta GET URL: /docs/rest-api-playground/2026-02-09/work-orders/workflow/schedule/eta-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Get the estimated time of arrival (ETA) and related timing information for this work order. --- ### Schedule GET URL: /docs/rest-api-playground/2026-02-09/work-orders/workflow/schedule/schedule-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve the service window and scheduling information for a work order. --- ### Schedule PUT URL: /docs/rest-api-playground/2026-02-09/work-orders/workflow/schedule/schedule-put {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Update the service window and scheduling information for a work order. --- ### Approve POST URL: /docs/rest-api-playground/2026-02-09/work-orders/workflow/workflow/approve-post {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Approve a work order after the work is completed and verified. --- ### Assignee POST URL: /docs/rest-api-playground/2026-02-09/work-orders/workflow/workflow/assignee-post {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Assign a provider (user) directly to a work order. Optionally include `clientPayTermsAccepted` to record acceptance of pay terms. --- ### Autodispatch POST URL: /docs/rest-api-playground/2026-02-09/work-orders/workflow/workflow/autodispatch-post {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Automatically dispatch a work order to a qualified provider based on configured dispatch rules. --- ### Complete DELETE URL: /docs/rest-api-playground/2026-02-09/work-orders/workflow/workflow/complete-delete {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Mark a completed work order as incomplete. A `reason` and `reason_id` are required to document the change. --- ### Draft DELETE URL: /docs/rest-api-playground/2026-02-09/work-orders/workflow/workflow/draft-delete {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Revert a work order back to draft. Use `async=true` for asynchronous processing. --- ### Mass route POST URL: /docs/rest-api-playground/2026-02-09/work-orders/workflow/workflow/mass-route-post {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Route a work order to multiple providers in a single request. Submit the list of provider IDs in the request body. --- ### Publish DELETE URL: /docs/rest-api-playground/2026-02-09/work-orders/workflow/workflow/publish-delete {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Unpublish a previously published work order, returning it to draft. Use `async=true` to process asynchronously. --- ### Publish POST URL: /docs/rest-api-playground/2026-02-09/work-orders/workflow/workflow/publish-post {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Move a draft work order to a published state so it becomes available for routing and assignment. Use `async=true` to process asynchronously. --- ### Route DELETE URL: /docs/rest-api-playground/2026-02-09/work-orders/workflow/workflow/route-delete {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Remove a previously routed provider (user) from a work order. Supports asynchronous processing via `async=true`. --- ### Route POST URL: /docs/rest-api-playground/2026-02-09/work-orders/workflow/workflow/route-post {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Route a work order to a single provider (user). Use `acting_user_id` when routing on behalf of another user, and `async=true` for async mode. --- ### Status GET URL: /docs/rest-api-playground/2026-02-09/work-orders/workflow/workflow/status-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Return the current status of a work order (including sub-status and flags). --- ## Webhooks Playground (12) ### Overview URL: /docs/webhooks-playground/2026-02-02/overview {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} --- ### Attributename DELETE URL: /docs/webhooks-playground/2026-02-02/attributes/attributename-delete {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Remove a specific webhook attribute (for example, a custom header or legacy field mapping) by its type and name. Use this endpoint to clean up attributes that are no longer needed on a webhook. --- ### Webhookid DELETE URL: /docs/webhooks-playground/2026-02-02/core-operations/webhookid-delete {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Delete a webhook configuration so it no longer receives notifications. The webhook's status is changed to 'archived'. Delivery logs and history remain available for auditing. --- ### Webhookid GET URL: /docs/webhooks-playground/2026-02-02/core-operations/webhookid-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve detailed information about a single webhook configuration, including subscribed events, status, and key settings. Use `fields` and `webhookAttribute` query parameters to control which core fields and additional attributes are returned. --- ### Webhookid PUT URL: /docs/webhooks-playground/2026-02-02/core-operations/webhookid-put {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Update an existing webhook configuration, such as the endpoint URL, HTTP method, subscribed events, status, or notification email. Only the fields you include in the request body are changed; omitted fields keep their current values. --- ### Webhooks GET URL: /docs/webhooks-playground/2026-02-02/core-operations/webhooks-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve a paginated list of webhooks configured for your company, with filters for status, search, sorting, and selected fields. Use this endpoint to quickly review and manage existing webhook configurations. --- ### Webhooks POST URL: /docs/webhooks-playground/2026-02-02/core-operations/webhooks-post {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Create a new webhook configuration for your company by providing the target URL, HTTP method, subscribed events, and status. After creation, the webhook receives notifications for the selected events and is assigned a unique `webhookId` for future operations. --- ### Delivery logs GET URL: /docs/webhooks-playground/2026-02-02/delivery-logs/delivery-logs-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve a paginated list of webhook delivery attempts, including successful deliveries, failures, and retries. Filter results by webhook ID, work order ID, event name, or delivery status to monitor reliability and troubleshoot issues. --- ### Deliveryid GET URL: /docs/webhooks-playground/2026-02-02/delivery-logs/deliveryid-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve detailed information about a single webhook delivery attempt, including status, timestamps, and a pre-signed URL to the full log file. Use this endpoint to investigate why a specific delivery succeeded or failed. --- ### Retry PATCH URL: /docs/webhooks-playground/2026-02-02/delivery-logs/retry-patch {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retry a previously failed webhook delivery by its `deliveryId`, creating a new delivery job for the same payload. Use this endpoint after fixing issues on your endpoint to reprocess specific events and track the retry by job ID. --- ### Events GET URL: /docs/webhooks-playground/2026-02-02/events/events-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} List all available webhook events that you can subscribe to, with basic details about each event. Use the optional `model` query parameter to filter events for a specific model type (for example, `WorkOrder` or `Provider`). --- ### History GET URL: /docs/webhooks-playground/2026-02-02/history/history-get {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve a paginated change history for a single webhook, including who made each change, what changed, and when it occurred. Use this audit trail to track configuration updates and troubleshoot issues. ---