Skip to main content

Receive webhooks

A FieldTask webhook sends a signed HTTPS POST to your receiver each time a customer, customer location, job or member changes. Each request body is one event.

Preview

Webhooks are part of a review contract that hasn't been released. FieldTask enables them separately from the rest of the API.

Before you start​

  • Webhooks and change sync enabled by FieldTask, with your organization enrolled for each record family you want. See Webhooks or polling?.

  • An organization owner or admin to add and manage the webhook in the FieldTask app. Webhooks can't be managed with an API token.

  • An API connection with read permissions for each family the webhook sends. A webhook belongs to one connection and is limited by its permissions:

    FamilyPermissions the connection needs
    Customerscustomers:read
    Customer locationscustomer_locations:read and customers:read
    Jobsjobs:read
    Membersmembers:read
  • A receiver that meets the requirements below, running with the signing secret before you verify it.

Receiver requirements​

FieldTask only delivers to a receiver that meets all of these:

RequirementDetail
HTTPS on port 443The URL must start with https:// and use port 443, the default. Other ports aren't accepted. The certificate must be valid for the host name.
Public addressThe host name must resolve only to public IP addresses. Private, loopback and reserved addresses are rejected.
Plain URLUp to 2,048 characters, with no user name or password and no #fragment. A query string is allowed.
Fast responseThe whole attempt, including connecting, TLS and reading your response, must finish within 5 seconds.
Small, uncompressed responseYour response body must be at most 4,096 bytes and must not be compressed. A larger or compressed response counts as a failure.
No redirectsRedirects aren't followed. A 3xx response is a failed attempt.

Each request is a POST with Content-Type: application/json, a body of at most 16 KiB, and the headers webhook-id, webhook-signature, webhook-timestamp.

To use a different URL later, add a new webhook. A webhook's URL can't be changed.

Add a webhook​

  1. Go to Settings → API connections, choose Manage for the connection, and in its Webhooks section choose Manage.
  2. Choose Add webhook.
  3. Enter the Receiver URL and select the Resources to send: Customers, Customer locations, Jobs and Members. Only families the connection can read are offered. Choose Add webhook.
  4. Copy the Signing secret. It starts with whsec_. It's shown once and can't be displayed again.
  5. Install the secret on your receiver and make sure the receiver is running.
  6. Select I have saved the secret and installed it on the receiver., choose Continue, then Confirm.

FieldTask then sends a signed verification challenge to your receiver. If your receiver answers correctly, the webhook becomes Active. If it doesn't, the webhook stays Pending; fix the receiver and choose Verify receiver in Delivery details.

  • You have 15 minutes from adding the webhook, and up to 5 verification attempts. After that, choose Disable webhook and add a new one. Do the same if you lose the secret before verifying.
  • Delivery starts from the moment verification succeeds. Changes made before that aren't sent, so backfill your copy with the list endpoints.
  • A connection can have up to 10 webhooks that aren't disabled.

Answer the verification challenge​

The challenge is a signed POST like any other delivery, with this body:

{"challenge":"EXAMPLE_CHALLENGE","type":"webhook.verification"}

Verify the signature, then reply within 5 seconds with any 2xx status and a body that is exactly:

{"challenge":"EXAMPLE_CHALLENGE"}

The reply is compared byte for byte: compact JSON, the challenge value copied unchanged, no spaces and no trailing newline. The samples below do this.

FieldTask sends a new challenge whenever it needs to confirm your receiver: when you add a webhook, resume or replay deliveries, and activate a new signing secret.

Verify each request​

Every request is signed with the Standard Webhooks scheme. Use a standardwebhooks library rather than writing the check yourself.

HeaderContains
webhook-idThe event ID. For deliveries it's the same as the body's event_id.
webhook-timestampWhen this attempt was signed, in seconds since the Unix epoch.
webhook-signatureOne or more signatures, separated by spaces, each in the form v1, followed by a Base64 HMAC-SHA256 value.

The signature covers the webhook-id, the webhook-timestamp and the raw request body, exactly as received. Verify before you parse:

  • Read the body as bytes. Don't parse it to JSON and serialize it again before verifying; a different byte sequence fails verification.
  • If your framework parses JSON automatically, read the raw body instead, for example request.get_data() in Flask or express.raw({ type: 'application/json' }) in Express.
  • Reject requests that fail verification with a 4xx status, and never with 410, which pauses the webhook.

Set FIELDTASK_WEBHOOK_SECRETS to your signing secret. During a secret rotation, set it to both secrets separated by a space. Every sample below reads it and tries each secret.

The Python and Node.js samples are complete receivers. The others give you a verify function and a handler that returns the status and body to send; call it from your web framework with the raw body and the request headers.

Install the libraries with pip install standardwebhooks flask.

receiver.py
import json
import os
from concurrent.futures import ThreadPoolExecutor

from flask import Flask, Response, request
from standardwebhooks import Webhook, WebhookVerificationError

# One or more signing secrets (whsec_...), separated by spaces.
WEBHOOKS = [Webhook(secret) for secret in os.environ["FIELDTASK_WEBHOOK_SECRETS"].split()]
SIGNED_HEADERS = ("webhook-id", "webhook-timestamp", "webhook-signature")


def verify_delivery(raw_body, headers):
"""Return the parsed body, or raise if no secret verifies the request."""
signed = {name: headers.get(name, "") for name in SIGNED_HEADERS}
error = None
for webhook in WEBHOOKS:
try:
return webhook.verify(raw_body, signed)
except (WebhookVerificationError, ValueError) as exc:
error = exc
raise error


app = Flask(__name__)
executor = ThreadPoolExecutor(max_workers=4) # use your job queue in production
processed = set() # use a durable store in production


def handle_event(event):
if event["event_id"] in processed:
return # a retry or replay you've already handled
processed.add(event["event_id"])
# Read the current record with the REST API, or remove it if event["tombstone"].
print(event["event_type"], event["resource"]["id"], event["resource"]["revision"])


@app.post("/fieldtask/webhooks")
def receive():
raw_body = request.get_data() # the exact bytes FieldTask signed
try:
message = verify_delivery(raw_body, request.headers)
except (WebhookVerificationError, ValueError):
return Response(status=400)
if message.get("type") == "webhook.verification":
# Reply with exactly {"challenge":"..."}: compact JSON and nothing else.
reply = json.dumps({"challenge": message["challenge"]}, separators=(",", ":"))
return Response(reply, status=200, content_type="application/json")
executor.submit(handle_event, message) # acknowledge first, then process
return Response(status=204)

Replay protection​

The standardwebhooks libraries reject a request whose webhook-timestamp is more than five minutes before or after your server's clock. Keep your clock synchronized. FieldTask signs every attempt with a fresh timestamp, so retries aren't rejected for being old.

Within those five minutes, a captured request could be sent to you again. Recording the event IDs you've processed, as the samples do, makes a repeated request harmless.

Respond quickly, process later​

Any 2xx response marks the delivery as delivered. Return it as soon as the request is verified, then do the work afterwards, for example by putting the event on a queue. Reading records from FieldTask or writing to your database before responding risks exceeding the 5 seconds limit, which counts as a failure and causes a retry.

Retries and failures​

A delivery attempt fails if your receiver times out, can't be reached, fails the requirements above, or returns any status other than 2xx. FieldTask then tries again with the same event ID and body, and a new signature and timestamp:

After failed attemptNext attempt no sooner than
115–30 seconds
230–60 seconds
31–2 minutes
42–4 minutes
54–8 minutes
68–16 minutes
716–32 minutes
8 to 1130–60 minutes

Each wait is chosen at random within its range. If you respond 429 or 503 with a Retry-After header in seconds, FieldTask waits at least that long, up to 1 hour.

FieldTask stops after 12 attempts, or 24 hours after the delivery was first queued or last replayed, whichever comes first. The webhook is then paused.

Delivery also ends early in these cases:

  • Your receiver returns 410 Gone. The delivery is marked as failed and the webhook pauses. Only return 410 if you want deliveries to stop.
  • The record is no longer visible to the connection when an attempt is due. Its created or updated delivery is cancelled. Its deleted event is still delivered.

Before each attempt, FieldTask checks the connection's access again. If the connection was revoked or its permissions changed, delivery stops and the webhook pauses.

FieldTask holds at most 1,000 waiting deliveries per webhook. When that many are waiting, it stops queuing new events until the backlog shrinks. Events aren't dropped; they're queued later.

Duplicates and ordering​

The same event can reach you more than once: after a timeout where your receiver did process the request, after a retry, or when someone replays a delivery. Events can also arrive out of order.

  • Deduplicate by webhook-id, which equals the event's event_id. Keep processed IDs for at least the history period, because deliveries can be replayed within it.
  • Use resource.revision to ignore an event that is older than the state you already have. See Revisions.

Pause, resume and replay​

Open Delivery details for a webhook to see its state, its last successful delivery and its recent deliveries, up to 100 per page.

Why a webhook pauses​

Reason shownWhat happenedWhat to do
The receiver returned HTTP 410.Your receiver answered 410.Fix the receiver, then Resume delivery.
Delivery retry limits were reached.A delivery failed 12 times or for 24 hours.Fix the receiver, then Resume delivery and select the failed deliveries to retry.
Connection permissions or sync access changed.The connection's permissions changed, the connection was revoked, or FieldTask changed sync access for your organization.This webhook can't be resumed. Disable webhook, add a new one, and resynchronize your copy from the list endpoints.
Required history expired.Waiting changes are older than the history FieldTask keeps.This webhook can't be resumed. Resynchronize your copy from the list endpoints, then add a new webhook.
Delivery was paused by an administrator.Someone chose Pause delivery.Resume delivery when ready.

When a webhook pauses for any reason other than an administrator's request, the organization's owner and admins get an in-app notification, Webhook delivery paused.

While a webhook is paused:

  • Deliveries that were waiting are marked Failed.
  • New changes aren't lost. They're sent after you resume, as long as they're still within the history period.

Resume delivery​

Resume delivery sends a new verification challenge to your receiver, then restarts delivery. You can select Failed or Delivered deliveries to send again; select none to resume with new changes only.

Replay selected deliveries​

On an Active webhook, select up to 100 Failed or Delivered deliveries and choose Replay selected deliveries. FieldTask verifies the receiver with a new challenge, then sends them again with their original event IDs and bodies. Each starts a new retry cycle of up to 12 attempts or 24 hours.

Only deliveries for events within the history period, for records the connection can still read, can be selected.

Limits​

Resuming, replaying, staging a signing secret and activating it share a limit of 5 per webhook in each 15-minute window. Choose Disable webhook to stop a webhook permanently; its waiting deliveries are cancelled.

Rotate the signing secret​

Rotate the secret if it may have been exposed, or on your own schedule. The webhook must be Active or Paused.

  1. In Delivery details, choose Stage new signing secret, then Confirm. Copy the new secret; it's shown once. The current secret keeps signing deliveries.

  2. Add the new secret to your receiver alongside the current one, for example FIELDTASK_WEBHOOK_SECRETS="whsec_CURRENT_PLACEHOLDER whsec_NEW_PLACEHOLDER". The samples above accept a request that verifies with either.

  3. Within 15 minutes of staging, choose Activate signing secret, then Confirm. FieldTask sends a verification challenge signed only with the new secret, so your receiver must already have it. The staged secret's expiry is shown as Staged secret expires; after it, stage a new one.

  4. After activation, the new secret signs every delivery. For 5 minutes, deliveries also carry a signature from the previous secret, so webhook-signature contains two values:

    webhook-signature: v1,SIGNATURE_WITH_NEW_SECRET v1,SIGNATURE_WITH_PREVIOUS_SECRET

    The end of this overlap is shown as Previous key signing ends.

  5. After the overlap ends, remove the previous secret from your receiver.

  • Staging again before activating replaces the staged secret.
  • You can't stage another secret until the overlap has ended.
  • This overlap only applies to webhook signing secrets. API token rotation works differently; see Authentication.