Docs / Webhooks

Webhooks & integrations

Send leads, jobs, estimates, inspection reports and invoices out of Siteflow Pro to GoHighLevel, Zapier, Make, n8n, Google Sheets, Slack, Discord or your own server, automatically.

Overview

A webhook is a web address (URL) that Siteflow Pro calls whenever something happens in your business, such as a new lead, a finished job or a paid invoice. Siteflow Pro sends a small JSON message to that URL, and the system on the other end (your CRM, an automation tool or your own server) does the rest.

  • Webhooks are outgoing: data goes from Siteflow Pro to other tools.
  • Only admins can create and manage them. Workers cannot see the Integrations screen.
  • They run on Siteflow’s servers, so events are sent even when nobody has the app open. Only data that has synced to your cloud account is sent. Edits made offline go out once they sync.
  • Each message is signed with a secret so your server can prove it really came from Siteflow Pro.
  • Webhook URLs must start with https://. You can connect up to 10 webhooks per business.

Sending data into Siteflow Pro

Siteflow Pro does not have a public inbound API yet, so other platforms cannot create or update jobs and clients in Siteflow Pro by calling it. See Receiving data into Siteflow Pro for what is and isn’t possible today.

Add a webhook (step by step)

You need an admin account signed in to your business (cloud) account in the Siteflow Pro mobile app.

  1. Open More → Integrations. You can also get there from Settings → Integrations → Webhooks & CRM. If you see “Admins only”, ask your business admin to do this step.
  2. Tap a platform card (GoHighLevel, Zapier, Make or Custom URL) or tap Add. The New Webhook sheet opens with short setup steps for that platform. Use Custom URL for n8n, Google Sheets relays, Slack/Discord relays or your own server.
  3. Fill in the form:
    FieldWhat to enter
    NameAny label so you can recognise it (up to 80 characters), for example “GHL – new leads”.
    Webhook URLThe address your platform gave you. It must start with https://. Siteflow Pro recognises GoHighLevel, Zapier and Make URLs automatically. Private network addresses such as localhost are rejected.
    EventsTick the events to send (see the event reference). Each webhook only receives the events you tick. At least one is required.
    ActiveOn by default. Switch it off to pause the webhook without deleting it.
  4. Tap Save Webhook. The webhook’s screen opens.
  5. Tap Send test (choose an event first if you subscribed to several). Siteflow Pro sends a sample message so your platform can learn the fields. See Testing.
  6. If your receiver verifies signatures, reveal and copy the Signing secret on the same screen. It starts with whsec_. See Verify the signature.

On the webhook’s screen you can also copy the URL, pause or resume it, edit it, change events, Regenerate secret (takes effect immediately, so update your receiver), see Recent deliveries with the status, HTTP code, response and payload of each attempt, and Delete webhook (this also removes its delivery log).

Event reference

EventFires whendata is a
client.createdA client is added, including a new lead created in the pipeline.client
client.updatedA client’s contact details, notes or status change.client
job.createdA job (or estimate visit) is created.job
job.updatedAnything meaningful on a job changes. Autosave timestamps and the work timer are ignored.job
job.completedA job’s status moves into Completed, Review, Closed or Paid.job
estimate.createdA job enters the Estimate Scheduled stage (pipeline_stage inspection_scheduled).job
report.createdA worker submits the inspection / estimate report.job
invoice.createdThe job’s invoice leaves “Not Ready”, i.e. an invoice is started.job
invoice.sentThe invoice status becomes “Sent”.job
invoice.paidThe job or invoice is marked paid.job

One save can produce several events. For example, submitting an inspection report can send job.updated, report.created and invoice.created together. Each webhook only gets the events it subscribes to. If you only care about milestones, skip job.updated, which fires on every meaningful save. Deletions are not sent.

Request format & example payloads

Siteflow Pro sends POST requests with a JSON body and these headers:

HeaderValue
X-Siteflow-EventThe event name, for example job.completed.
X-Siteflow-DeliveryA unique id for this delivery (same as id in the body). Retries reuse it, so use it to ignore duplicates.
X-Siteflow-Attempt1 to 4.
X-Siteflow-TimestampUnix time (seconds) when the attempt was sent. It is informational and is not part of the signature.
X-Siteflow-Signaturesha256= followed by the hex HMAC-SHA256 of the raw body, keyed with the webhook’s signing secret.
User-AgentSiteflowPro-Webhooks/1.0
Headers
POST /webhooks/siteflow HTTP/1.1
Content-Type: application/json
User-Agent: SiteflowPro-Webhooks/1.0
X-Siteflow-Event: job.completed
X-Siteflow-Delivery: 5d2b8f0a-77c4-4c1e-b0a5-91e3a6c2d4f8
X-Siteflow-Attempt: 1
X-Siteflow-Timestamp: 1791014700
X-Siteflow-Signature: sha256=<64 hex characters>

The body always has this envelope:

KeyDescription
idUnique delivery id.
eventEvent name.
occurred_atISO 8601 time of the change.
business_id, businessYour business: { id, name, email, phone }.
testPresent and true only on “Send test” deliveries.
dataThe client or job after the change, as a flat object (see the field reference). Flat fields make it easy to map in GoHighLevel, Zapier and Make.
previousThe same shape, from before the change. Only present when something was updated (client.updated and every job event except job.created).

client.created

JSON
{
  "id": "b3f0c1de-5a8e-4f7e-9c1a-2d4e6f8a0b1c",
  "event": "client.created",
  "occurred_at": "2026-10-03T07:30:00.000Z",
  "business_id": "6f1c2d3e-0000-4000-8000-00000000b001",
  "business": {
    "id": "6f1c2d3e-0000-4000-8000-00000000b001",
    "name": "Rivera Remodeling",
    "email": "office@rivera-remodel.example",
    "phone": "<business phone, if set>"
  },
  "data": {
    "object": "client",
    "id": "00000000-0000-4000-8000-000000000001",
    "client_id": "00000000-0000-4000-8000-000000000001",
    "contact_name": "Jamie Rivera",
    "contact_first_name": "Jamie",
    "contact_last_name": "Rivera",
    "contact_email": "jamie.rivera@example.com",
    "contact_phone": "<customer phone as entered>",
    "contact_address": "42 Elm Street, Austin, TX 78701",
    "client_status": "active",
    "notes": "Referred by a neighbour. Wants a kitchen remodel quote.",
    "source": "Siteflow Pro",
    "created_at": "2026-10-03T07:30:00.000Z",
    "updated_at": "2026-10-03T07:30:00.000Z"
  }
}

invoice.paid (all job, estimate, report and invoice events use this shape)

JSON
{
  "id": "5d2b8f0a-77c4-4c1e-b0a5-91e3a6c2d4f8",
  "event": "invoice.paid",
  "occurred_at": "2026-10-03T07:30:00.000Z",
  "business_id": "6f1c2d3e-0000-4000-8000-00000000b001",
  "business": {
    "id": "6f1c2d3e-0000-4000-8000-00000000b001",
    "name": "Rivera Remodeling",
    "email": "office@rivera-remodel.example",
    "phone": "<business phone, if set>"
  },
  "data": {
    "object": "job",
    "id": "00000000-0000-4000-8000-000000000002",
    "job_id": "00000000-0000-4000-8000-000000000002",
    "client_id": "00000000-0000-4000-8000-000000000001",
    "contact_name": "Jamie Rivera",
    "contact_first_name": "Jamie",
    "contact_last_name": "Rivera",
    "contact_email": "jamie.rivera@example.com",
    "contact_phone": "<customer phone as entered>",
    "contact_address": "42 Elm Street, Austin, TX 78701",
    "job_title": "Kitchen remodel",
    "job_type": "Remodel",
    "job_status": "Paid",
    "job_address": "42 Elm Street, Austin, TX 78701",
    "workflow_type": "inspection_first",
    "pipeline_stage": "paid",
    "pipeline_stage_label": "Closed / Paid",
    "start_date": "2026-10-03",
    "due_date": "2026-10-03",
    "assigned_worker_ids": [],
    "notes": "Customer prefers morning appointments.",
    "currency": "USD",
    "amount": 5200,
    "estimate_amount": 4800,
    "invoice_amount": 5200,
    "final_amount": 5200,
    "deposit_paid": 1000,
    "amount_received": null,
    "payment_status": "Paid",
    "payment_method": "",
    "invoice_status": "Paid",
    "invoice_number": "INV-1042",
    "invoice_issue_date": "2026-10-03",
    "invoice_due_date": "2026-10-03",
    "invoice_sent_at": null,
    "invoice_accepted_at": null,
    "invoice_notes": "Thank you for your business.",
    "contract_status": "Sent",
    "line_items": [
      { "description": "Cabinet install", "type": "labor", "quantity": 1, "rate": 3200, "amount": 3200 },
      { "description": "Quartz countertop", "type": "material", "quantity": 1, "rate": 2000, "amount": 2000 }
    ],
    "report_status": "Submitted",
    "report_submitted_at": "2026-10-01T15:12:00.000Z",
    "report_summary": "Cabinets need replacing; plumbing is in good condition.",
    "report_work_needed": "Remove old cabinets, install new units and countertop.",
    "report_recommended_service": "Full kitchen remodel",
    "report_materials_needed": "Cabinets, quartz countertop, hardware",
    "report_estimated_labor_hours": 32,
    "report_customer_concerns": "",
    "report_safety_issues": "",
    "report_photo_urls": [],
    "inspection_completed_at": null,
    "customer_accepted_at": null,
    "work_started_at": null,
    "created_at": "2026-10-03T07:30:00.000Z",
    "updated_at": "2026-10-03T07:30:00.000Z",
    "source": "Siteflow Pro"
  }
}

Update events and previous

Update and transition events include previous, so you can see what changed. This is a shortened illustration:

Shape (shortened)
{
  "id": "…",
  "event": "job.completed",
  "occurred_at": "2026-10-03T09:05:00.000Z",
  "business_id": "…",
  "business": { "id": "…", "name": "…", "email": "…", "phone": "…" },
  "data":     { "object": "job", "job_status": "Completed", "pipeline_stage": "job_completed_by_worker", "…": "all job fields, after the change" },
  "previous": { "object": "job", "job_status": "In Progress", "pipeline_stage": "job_in_progress", "…": "all job fields, before the change" }
}

Phone fields in the examples are placeholders. Real payloads contain the numbers exactly as they were entered in the app (or an empty string).

Field reference

Client events (data.object = "client")

FieldTypeNotes
objectstringAlways "client".
id, client_idstring (uuid)The client’s id. Both fields hold the same value.
contact_namestringFull name as entered.
contact_first_name, contact_last_namestringThe name split on the first space. The last name is empty for single-word names.
contact_email, contact_phone, contact_addressstringContact details as entered in the app. Empty string when not set.
client_statusstringClient status, "active" by default.
notesstringFree-text notes.
sourcestringAlways "Siteflow Pro".
created_at, updated_atstring (ISO 8601) | nullRecord timestamps.

Job, estimate, report and invoice events (data.object = "job")

FieldTypeNotes
objectstringAlways "job". Jobs also carry the estimate, report and invoice state.
id, job_id, client_idstring (uuid) | nullJob id (twice) and the linked client id.
contact_name, contact_first_name, contact_last_name, contact_email, contact_phone, contact_addressstringThe customer on the job. Use these to create or update a CRM contact. The address is the job address.
job_title, job_type, job_status, job_address, workflow_typestringJob basics. job_status is the app’s status (for example Scheduled, In Progress, Completed, Paid).
pipeline_stage, pipeline_stage_labelstringInternal pipeline stage and a friendly label such as "New Lead", "Estimate Scheduled", "Estimate Sent", "Job Won / Deposit", "Job In Progress", "Invoice Sent" or "Closed / Paid".
start_date, due_datestring | nullJob dates.
assigned_worker_idsstring[]Ids of the workers assigned to the job.
notesstringJob notes.
currencystringAlways "USD".
amountnumber | nullBest single amount: the final amount if set, otherwise the invoice amount, otherwise the estimate.
estimate_amount, invoice_amount, final_amount, deposit_paidnumber | nullMoney fields.
amount_receivednumber | nullCash received plus bank received, or null when nothing is recorded.
payment_status, payment_methodstringPayment state, for example "Paid" or "Unpaid".
invoice_status, invoice_number, invoice_issue_date, invoice_due_date, invoice_sent_at, invoice_accepted_at, invoice_notes, contract_statusstring | nullInvoice state. invoice_status is "Not Ready" until an invoice is started.
line_itemsarrayUp to 100 items: { description, type, quantity, rate, amount } where amount = quantity × rate.
report_status, report_submitted_at, report_summary, report_work_needed, report_recommended_service, report_materials_needed, report_estimated_labor_hours, report_customer_concerns, report_safety_issues, report_photo_urlsstring | number | arrayFields from the inspection / estimate report. report_photo_urls is an array of photo URLs.
inspection_completed_at, customer_accepted_at, work_started_at, created_at, updated_atstring | nullTimestamps.
sourcestringAlways "Siteflow Pro".

Fields with no value are sent as an empty string (text) or null (numbers and dates).

Verify the signature

Every request carries X-Siteflow-Signature. To confirm it came from Siteflow Pro, compute an HMAC-SHA256 of the raw request body (the exact bytes, before JSON parsing) using the webhook’s signing secret, then compare it to the value after sha256= using a constant-time comparison. Copy or regenerate the secret on the webhook’s screen in the app.

Parsing the JSON first and re-serialising it changes the bytes and breaks the check. Always hash the raw body. Because the timestamp header isn’t signed, dedupe on X-Siteflow-Delivery if you need replay protection.
Node.js (Express)
import crypto from 'node:crypto';
import express from 'express';

const app = express();
const SECRET = process.env.SITEFLOW_WEBHOOK_SECRET; // whsec_…

// Use the RAW body. Do not put express.json() in front of this route.
app.post('/webhooks/siteflow', express.raw({ type: 'application/json' }), (req, res) => {
  const expected = 'sha256=' + crypto.createHmac('sha256', SECRET).update(req.body).digest('hex');
  const received = req.get('X-Siteflow-Signature') || '';
  const ok = received.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
  if (!ok) return res.status(401).send('bad signature');

  const delivery = req.get('X-Siteflow-Delivery'); // dedupe on this, retries reuse it
  const payload = JSON.parse(req.body.toString('utf8'));
  console.log(delivery, payload.event, payload.data.contact_email);

  res.sendStatus(204); // reply 2xx fast, do slow work after responding
});

app.listen(3000);
Python (Flask)
import hashlib
import hmac
import os

from flask import Flask, request, abort

app = Flask(__name__)
SECRET = os.environ["SITEFLOW_WEBHOOK_SECRET"]  # whsec_...


def verify(raw_body: bytes, header: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header or "")


@app.post("/webhooks/siteflow")
def siteflow_webhook():
    raw = request.get_data()  # the raw bytes, before any JSON parsing
    if not verify(raw, request.headers.get("X-Siteflow-Signature"), SECRET):
        abort(401)

    delivery_id = request.headers.get("X-Siteflow-Delivery")  # dedupe on this
    payload = request.get_json(force=True)
    print(delivery_id, payload["event"], payload["data"].get("contact_email"))
    return "", 204

Test your implementation against this known value:

Check value
Secret: whsec_test
Body:   {"event":"test"}
Header: sha256=a3360ff33a49b99ab3599fe06973a1dc6d6f0f859e8cd23784f27fbe8709cef2

GoHighLevel, Zapier and Make do not verify signatures. Their webhook URLs act like passwords, so keep them private. If you use a signature-checking receiver (your own server or a relay), store the secret in an environment variable, never in public code.

Delivery, retries & the delivery log

  • Reply with any 2xx status within 10 seconds. The response body is ignored, but the first 1,000 characters are shown in the app.
  • Timeouts, network errors and 408, 429 and 5xx responses are retried up to 3 times, after 2, 6 and 18 seconds (4 attempts in total).
  • Other responses, including 4xx and redirects (3xx), are treated as permanent failures and are not retried. Siteflow Pro does not follow redirects.
  • Retries reuse the same X-Siteflow-Delivery id. Delivery is at-least-once, and events can arrive out of order when a retry is involved, so make your handler idempotent.
  • Every delivery is logged and shown under Recent deliveries on the webhook’s screen (status, HTTP code, attempts, response snippet and payload). Entries older than 30 days are removed.
  • If Siteflow’s own dispatch step is unreachable at the moment of the change, that event is not re-queued. Failures at your endpoint are retried as described above.

Testing

  1. Open the webhook in More → Integrations and scroll to Send test. If the webhook has several events, pick the one to simulate.
  2. Tap Send test. Siteflow Pro sends one realistic sample (a made-up customer, job and invoice) marked "test": true, signed like a real delivery, with a single attempt and no retries.
  3. The result card shows the HTTP status, how long it took and the first part of your endpoint’s response.
  4. In your platform, load the sample (for example “Fetch sample”, “Test trigger” or “Run once”, see the platform guides) and map the fields.
  5. For a free way to inspect requests, create a temporary endpoint with a request-bin service such as webhook.site, add it as a Custom URL webhook, and tap Send test.
  6. Finish with a real check: add a client or create a job in the app and confirm the event shows up under Recent deliveries and in your platform.

Platform guides

All of these send data out of Siteflow Pro. Button and menu names in other products change from time to time, so follow the idea if a label looks different.

GoHighLevel

  1. In GoHighLevel go to Automation → Workflows → Create Workflow → Start from scratch.
  2. Add New Trigger → Inbound Webhook. Copy the URL it shows (it starts with https://services.leadconnectorhq.com/hooks/).
  3. In Siteflow Pro open More → Integrations → GoHighLevel, paste the URL and pick events, for example client.created, job.created, estimate.created, job.completed and invoice.paid. Save.
  4. Tap Send test in the app, then in GoHighLevel click Fetch sample request and choose the test request.
  5. Add a Create/Update Contact action and map contact_first_name, contact_last_name, contact_email, contact_phone and contact_address from the sample.
  6. Optional: add tags, create an opportunity from job_title, amount and pipeline_stage_label, or branch with If/Else on event to do different things for new leads, completed jobs and payments.
  7. Publish the workflow.

Use one workflow per event, or one workflow with If/Else on event. GoHighLevel treats Inbound Webhook as a premium trigger, which may be billed per execution depending on your plan, so check your GoHighLevel pricing.

Zapier

  1. Create a Zap. Trigger: Webhooks by Zapier → Catch Hook. Leave “Pick off a child key” empty.
  2. Copy the custom webhook URL (https://hooks.zapier.com/hooks/catch/…).
  3. In Siteflow Pro open More → Integrations → Zapier, paste the URL, choose events and save. Tap Send test.
  4. In Zapier click Test trigger and pick the request. Fields appear with names such as Data Contact Email and Data Amount.
  5. Add your action: HubSpot, Pipedrive, Google Sheets, GoHighLevel, Slack, Gmail and so on. Add a Filter on Event if the Zap should only handle certain events.

“Webhooks by Zapier” is a premium Zapier app, so it needs a paid Zapier plan or trial.

Make

  1. Create a scenario and add the Webhooks → Custom webhook module. Click Add, name it and copy the address.
  2. In Siteflow Pro open More → Integrations → Make, paste the address and save.
  3. In Make click Run once so it is listening, then tap Send test in the app. Make learns the data structure from that request.
  4. Add modules (HubSpot, Google Sheets, Slack, email…) and map the data.* fields. Use a Router with filters on event for separate paths per event.
  5. Turn the scenario On (scheduling set to immediately) so it keeps receiving events.

n8n

  1. Create a workflow and add a Webhook node. Set HTTP Method to POST and give it a path, for example siteflow.
  2. The node shows a Test URL and a Production URL. While building, click Listen for test event and use the Test URL.
  3. In Siteflow Pro choose Custom URL, paste the URL, pick events and save. Tap Send test. n8n receives the JSON under body (for example {{ $json.body.data.contact_email }}).
  4. Add your nodes: HubSpot, Google Sheets, Slack, Discord, HTTP Request to any API. Use an IF or Switch node on body.event for per-event logic.
  5. When it works, activate the workflow and edit the webhook in Siteflow Pro to use the Production URL. The Test URL only works while you are listening in the editor.

n8n must be reachable over public HTTPS (n8n Cloud, or a self-hosted instance with a public https address).

Google Sheets

Siteflow Pro can’t write to a spreadsheet directly. Use Zapier, Make or n8n as the bridge. A common setup is one row per paid invoice or per new lead.

  1. Create a sheet with headers such as Date, Customer, Email, Job, Amount, Status.
  2. Set up the catch step from the Zapier, Make or n8n guide above, subscribing the Siteflow webhook to events such as client.created or invoice.paid.
  3. Add a Google Sheets → Add/Create Row action and map: Date ← occurred_at, Customer ← data.contact_name, Email ← data.contact_email, Job ← data.job_title, Amount ← data.amount, Status ← data.pipeline_stage_label.
  4. Send a test, check the row appears, then turn the automation on.

Pointing a webhook straight at a Google Apps Script web app is not recommended. Those endpoints usually answer with a redirect, which Siteflow Pro does not follow, so the delivery would be logged as failed even if the script ran.

Slack and Discord

Slack and Discord incoming-webhook URLs only accept their own message format ({"text": "…"} for Slack, {"content": "…"} for Discord). Siteflow Pro sends its own JSON, so do not paste a Slack or Discord webhook URL directly. They would reject the message. Instead:

Option A: no code (recommended)

  1. Build a Zapier, Make or n8n catch step as above.
  2. Add a Slack → Send Channel Message or Discord → Send Message action.
  3. Write the message from mapped fields, for example “Paid! $amount from contact_name for job_title”.

Option B: your own tiny relay

If you prefer to run code, this relay verifies the Siteflow signature and posts to Slack and/or Discord. Add its public https URL in Siteflow Pro as a Custom URL webhook.

Node.js relay
// Tiny relay: verifies Siteflow's signature, then posts a message to Slack or Discord.
// Slack and Discord incoming webhooks only accept their own JSON shape, so they can't be
// pasted into Siteflow directly. Deploy this anywhere that serves HTTPS (Render, Fly.io, Cloud Run, a VPS...).
import crypto from 'node:crypto';
import express from 'express';

const app = express();
const SECRET = process.env.SITEFLOW_WEBHOOK_SECRET;
const SLACK_URL = process.env.SLACK_WEBHOOK_URL;     // https://hooks.slack.com/services/...
const DISCORD_URL = process.env.DISCORD_WEBHOOK_URL; // https://discord.com/api/webhooks/...

const money = (n) => (n == null ? 'n/a' : '$' + Number(n).toLocaleString('en-US'));

function describe({ event, data }) {
  const who = data.contact_name || 'a customer';
  switch (event) {
    case 'client.created': return 'New lead: ' + who + ' (' + (data.contact_email || 'no email') + ')';
    case 'job.created': return 'New job: ' + data.job_title + ' for ' + who;
    case 'job.completed': return 'Job completed: ' + data.job_title + ' for ' + who;
    case 'invoice.sent': return 'Invoice ' + data.invoice_number + ' sent to ' + who + ' (' + money(data.amount) + ')';
    case 'invoice.paid': return 'Paid! ' + money(data.amount) + ' from ' + who + ' for ' + data.job_title;
    default: return event + ': ' + (data.job_title || data.contact_name || '');
  }
}

app.post('/siteflow', express.raw({ type: 'application/json' }), async (req, res) => {
  const expected = 'sha256=' + crypto.createHmac('sha256', SECRET).update(req.body).digest('hex');
  const received = req.get('X-Siteflow-Signature') || '';
  if (received.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected))) {
    return res.status(401).send('bad signature');
  }
  const text = describe(JSON.parse(req.body.toString('utf8')));
  const posts = [];
  if (SLACK_URL) posts.push(fetch(SLACK_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text }) }));
  if (DISCORD_URL) posts.push(fetch(DISCORD_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content: text }) }));
  await Promise.allSettled(posts);
  res.sendStatus(204);
});

app.listen(process.env.PORT || 3000);

HubSpot, Pipedrive and other CRMs

Use the same pattern: catch the webhook in Zapier, Make or n8n, then add that CRM’s “create or update contact / deal” action. Map contact_email as the matching key so repeat customers update instead of duplicating, and use event to decide whether to create a contact (client.created), move a deal (estimate.created, job.completed) or mark it won (invoice.paid).

Your own server

Use Custom URL with any public https endpoint that accepts a JSON POST. Verify X-Siteflow-Signature (see the Node and Python samples above), reply 2xx quickly, and dedupe on X-Siteflow-Delivery.

Receiving data into Siteflow Pro

Not available yet

Siteflow Pro currently has no public inbound webhook or REST API. Other platforms cannot create or update clients, jobs or invoices in Siteflow Pro by calling it. Siteflow Pro only sends webhooks out.

Here is what works today:

  • Siteflow Pro → other platforms: fully supported by webhooks, as described on this page. Pair a webhook with Zapier, Make or n8n to push data into almost any tool (CRMs, spreadsheets, chat, email, accounting).
  • Other platforms → Siteflow Pro: add the lead, client or job in the app yourself (for example from the Clients or Pipeline screens). A two-way sync is not possible at the moment.

Need to push data in automatically? Tell us at Support what you want to connect. Demand helps us decide what to build next.

Troubleshooting

Start on the webhook’s screen: the status pill, last attempt message and Recent deliveries usually show what happened.

ProblemWhat to check
Nothing arrivesIs the webhook Active? Is the event ticked? Are you signed in as an admin to a cloud account? Has the change synced from the phone (offline edits are sent after they sync)? Try Send test first to separate setup problems from event problems.
“HTTP 400/401/403/404”Your endpoint rejected the request. These are not retried. Check the URL for typos, that it accepts POST with a JSON body, and that your signature check uses the raw body and the current secret.
“HTTP 3xx”The URL redirects. Siteflow Pro does not follow redirects, so use the final URL (watch for http → https and trailing-slash redirects).
“HTTP 5xx” or “Timed out after 10s”Your endpoint is down or too slow. Siteflow Pro retries up to 3 more times. Reply 2xx first and do slow work afterwards.
URL points to a private host / must start with https://Only public https URLs work. localhost, private IPs and addresses with embedded credentials are rejected. For local development use a tunnel that gives you a public https URL.
Signature doesn’t matchHash the raw bytes, not re-serialised JSON. Use the secret from this webhook (each webhook has its own). If you regenerated the secret, update your receiver. Compare against the full header including the sha256= prefix.
Slack or Discord returns 400They don’t accept Siteflow’s JSON shape. Use Zapier/Make/n8n or the relay in the Slack and Discord guide.
Make or Zapier doesn’t see the testIn Make press Run once before sending the test. In Zapier press Test trigger after sending. In n8n use the Test URL while listening, then switch Siteflow Pro to the Production URL once the workflow is active.
DuplicatesOne save can produce several events (for example job.updated plus invoice.paid), and retries resend the same delivery. Filter on the event and dedupe on X-Siteflow-Delivery.
Too many job.updated messagesUntick job.updated and subscribe to milestone events such as job.completed, invoice.sent and invoice.paid.
“Limit reached”A business can have 10 webhooks. Delete ones you no longer use, or send one webhook to an automation tool that fans out.
Deleted recordsDeletions are not sent as events.

Still stuck? Email us from Support with the webhook’s name, the event and the time of the failed delivery. Never send us your signing secret.

Limits & good to know

  • Up to 10 webhooks per business. Only admins can manage them.
  • HTTPS only. Private, loopback, link-local and metadata addresses are blocked.
  • 10-second timeout per attempt, at most 4 attempts per delivery, 30-day delivery log.
  • Inspection photos are sent as URLs in report_photo_urls, never as file attachments.
  • Only cloud-synced data is sent. Offline edits are sent when the phone syncs.
  • Edits made in the app are saved after a short pause and combined, so one editing session produces a single job.updated, not one per keystroke.
  • Recording a payment updates amount_received, payment_status and invoice_status together; the last payment also fires invoice.paid.
  • Webhooks are outgoing only. There is no inbound API yet. See Integrations & data flow for how to combine tools.