Webhook payload structure for job status changes

Hey team — I'm building out a notification pipeline that needs to fire when work orders hit specific status transitions (Created → Assigned → In Progress → Completed, etc.).

I've got webhooks configured and hitting my endpoint, but the payload structure I'm receiving doesn't seem to match what's in the webhook setup guide. Specifically:

  • Is job_id always present, or only for certain event types?
  • Does the payload include the full work order object, or just the delta/changed fields?
  • Are status transition events (work_order.status_changed) supposed to include the previous status value somewhere? I'm seeing status but not previous_status or similar.

Also heads up — I'm seeing inconsistent behavior between the sandbox and prod environments. Sandbox payloads include a technician_ids array, but prod is sending assigned_to as a single string. YMMV warning here.

My use case: need to sync status changes to an external CMDB and trigger Slack notifications when jobs hit "Completed." The work_order.completed event seems to fire before photos finish uploading sometimes, which is... not ideal for our downstream workflow.

Anyone got a canonical payload example for work_order.status_changed? Or should I be subscribing to individual status events instead of the generic change event?

  • yeah this one tripped me up too when I first looked at it

    so the work_order.status_changed event is actually a bit of a trap — it's generic by design but the payload structure depends on what triggered the change. here's what I've seen:

    {
      "event": "work_order.status_changed",
      "timestamp": "2025-09-17T10:15:00Z",
      "data": {
        "work_order_id": "wo_abc123",
        "status": "completed",
        // previous_status is NOT guaranteed — see below
        "changed_by": {
          "user_id": "usr_def456",
          "source": "mobile_app" // or "web", "api", "system"
        },
        "technician_ids": ["tech_ghi789"], // array in v2.1+, string in legacy
        "context": {
          // sometimes has previous_status here, sometimes doesn't
        }
      }
    }

    the previous_status thing is annoying — it's in the context object when the change comes from the web app, but missing when it comes from mobile sync. I opened a ticket about this a while back, still waiting.

    worth noting: if you need reliable state comparison, you really should cache the previous state yourself. I know, I know, but that's the workaround rn.

    also re: the completed event firing early — that's a known race condition with photo uploads. the event fires when status transitions, not when all attachments finish. there's a separate work_order.attachments_complete event in beta that might help? v2.1 webhook improvements mentions it briefly.

    • job_id → never present. Use work_order_id.
    • Full object vs delta → delta only. Subscribe to work_order.updated if you need full object.
    • Previous status → not in contract. Do not rely on it.

    Environment inconsistency: prod is on v2.1, most sandboxes still on v2.0. technician_ids vs assigned_to is the version delta.

    Docs are wrong here: Setting Up Webhooks shows v2.0 structure. Engineering ticket #4421 to update.

  • Ah, that explains the sandbox/prod mismatch — thanks. Is there a migration path for sandboxes or should I just build defensively against both formats?

    Also any ETA on that docs update? Trying to get this pipeline to production this week and having to reverse engineer the schema is... not my favorite Monday activity.

  • Can you share your current webhook subscription config and a sample payload you're receiving? The field mapping varies by subscription scope — if you're subscribed at account level vs filtered by technician or job type, the payload shape changes.

    Also need to know: are you verifying webhook signatures or just accepting raw POST? If you're not verifying, start there — I've seen proxies modify payloads in transit.

  • defensive is the way to go — I just normalize both:

    const technicianIds = Array.isArray(payload.data.technician_ids) 
      ? payload.data.technician_ids 
      : [payload.data.assigned_to].filter(Boolean);

    re: docs — lol no idea on ETA. the actual OpenAPI spec is more reliable than the help articles: api.fieldpulse.com/.../openapi.json

    pro tip: if you need photo-complete guarantees, poll GET /work_orders/{id} with ?include=attachments after getting the completed event. not elegant but it works. until that attachments_complete event ships anyway.

  • Hi Devon! Happy to help clarify this — the webhook payload structure has evolved and I know the documentation hasn't kept pace in all places.

    For status change events specifically:

    1. Always use work_order_idjob_id is legacy terminology that shouldn't appear in v2.x webhooks
    2. The work_order.status_changed event sends a delta payload, not the full object. If you need the complete work order, you'll want to subscribe to work_order.updated or make a follow-up API call
    3. Previous status — this is available in the context object, but only when the change originates from the web application. Mobile and API updates don't currently populate this. We're tracking this gap internally

    Regarding your environment differences:

    You're correct — sandbox environments are running an older webhook version. The technician_ids array format is the current standard in production (v2.1). You can request a sandbox refresh to v2.1 through your CSM, or code defensively as Eli suggested.

    For the photo upload race condition:

    This is a known limitation. The work_order.completed event fires on status transition, not media completion. For your CMDB sync use case, I'd recommend:

    1. Subscribe to work_order.status_changed
    2. Filter for status: "completed"
    3. Queue a delayed job (30-60 seconds) or poll the Work Orders API to verify attachments before notifying Slack

    I've attached this to your account notes and we'll follow up when the attachments-complete event moves to GA. Let me know if you need a canonical payload example for your security review!

  • Update: implemented the defensive parsing and it's working well. The delayed queue approach for photo verification is solid — using 45s delay and checking attachments_complete via API before firing Slack notifications.

    One gotcha I ran into: the context.previous_status field is camelCase (previousStatus) in some payloads and snake_case in others. Normalizing to lowercase before comparison now.

    Thanks all for the help — this thread saved me a lot of debugging time. Also cross-posting my API rate limit findings since the bulk creation piece relates.