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
Add a webhook (step by step)
You need an admin account signed in to your business (cloud) account in the Siteflow Pro mobile app.
- 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.
- 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.
- Fill in the form:
Field What to enter Name Any label so you can recognise it (up to 80 characters), for example “GHL – new leads”. Webhook URL The 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.Events Tick the events to send (see the event reference). Each webhook only receives the events you tick. At least one is required. Active On by default. Switch it off to pause the webhook without deleting it. - Tap Save Webhook. The webhook’s screen opens.
- 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.
- 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
| Event | Fires when | data is a |
|---|---|---|
client.created | A client is added, including a new lead created in the pipeline. | client |
client.updated | A client’s contact details, notes or status change. | client |
job.created | A job (or estimate visit) is created. | job |
job.updated | Anything meaningful on a job changes. Autosave timestamps and the work timer are ignored. | job |
job.completed | A job’s status moves into Completed, Review, Closed or Paid. | job |
estimate.created | A job enters the Estimate Scheduled stage (pipeline_stage inspection_scheduled). | job |
report.created | A worker submits the inspection / estimate report. | job |
invoice.created | The job’s invoice leaves “Not Ready”, i.e. an invoice is started. | job |
invoice.sent | The invoice status becomes “Sent”. | job |
invoice.paid | The 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:
| Header | Value |
|---|---|
X-Siteflow-Event | The event name, for example job.completed. |
X-Siteflow-Delivery | A unique id for this delivery (same as id in the body). Retries reuse it, so use it to ignore duplicates. |
X-Siteflow-Attempt | 1 to 4. |
X-Siteflow-Timestamp | Unix time (seconds) when the attempt was sent. It is informational and is not part of the signature. |
X-Siteflow-Signature | sha256= followed by the hex HMAC-SHA256 of the raw body, keyed with the webhook’s signing secret. |
User-Agent | SiteflowPro-Webhooks/1.0 |
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:
| Key | Description |
|---|---|
id | Unique delivery id. |
event | Event name. |
occurred_at | ISO 8601 time of the change. |
business_id, business | Your business: { id, name, email, phone }. |
test | Present and true only on “Send test” deliveries. |
data | The 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. |
previous | The same shape, from before the change. Only present when something was updated (client.updated and every job event except job.created). |
client.created
{
"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)
{
"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:
{
"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")
| Field | Type | Notes |
|---|---|---|
object | string | Always "client". |
id, client_id | string (uuid) | The client’s id. Both fields hold the same value. |
contact_name | string | Full name as entered. |
contact_first_name, contact_last_name | string | The name split on the first space. The last name is empty for single-word names. |
contact_email, contact_phone, contact_address | string | Contact details as entered in the app. Empty string when not set. |
client_status | string | Client status, "active" by default. |
notes | string | Free-text notes. |
source | string | Always "Siteflow Pro". |
created_at, updated_at | string (ISO 8601) | null | Record timestamps. |
Job, estimate, report and invoice events (data.object = "job")
| Field | Type | Notes |
|---|---|---|
object | string | Always "job". Jobs also carry the estimate, report and invoice state. |
id, job_id, client_id | string (uuid) | null | Job id (twice) and the linked client id. |
contact_name, contact_first_name, contact_last_name, contact_email, contact_phone, contact_address | string | The 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_type | string | Job basics. job_status is the app’s status (for example Scheduled, In Progress, Completed, Paid). |
pipeline_stage, pipeline_stage_label | string | Internal 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_date | string | null | Job dates. |
assigned_worker_ids | string[] | Ids of the workers assigned to the job. |
notes | string | Job notes. |
currency | string | Always "USD". |
amount | number | null | Best single amount: the final amount if set, otherwise the invoice amount, otherwise the estimate. |
estimate_amount, invoice_amount, final_amount, deposit_paid | number | null | Money fields. |
amount_received | number | null | Cash received plus bank received, or null when nothing is recorded. |
payment_status, payment_method | string | Payment 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_status | string | null | Invoice state. invoice_status is "Not Ready" until an invoice is started. |
line_items | array | Up 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_urls | string | number | array | Fields 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_at | string | null | Timestamps. |
source | string | Always "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.
X-Siteflow-Delivery if you need replay protection.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);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 "", 204Test your implementation against this known value:
Secret: whsec_test
Body: {"event":"test"}
Header: sha256=a3360ff33a49b99ab3599fe06973a1dc6d6f0f859e8cd23784f27fbe8709cef2GoHighLevel, 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,429and5xxresponses are retried up to 3 times, after 2, 6 and 18 seconds (4 attempts in total). - Other responses, including
4xxand redirects (3xx), are treated as permanent failures and are not retried. Siteflow Pro does not follow redirects. - Retries reuse the same
X-Siteflow-Deliveryid. 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
- Open the webhook in More → Integrations and scroll to Send test. If the webhook has several events, pick the one to simulate.
- 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. - The result card shows the HTTP status, how long it took and the first part of your endpoint’s response.
- In your platform, load the sample (for example “Fetch sample”, “Test trigger” or “Run once”, see the platform guides) and map the fields.
- 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.
- 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
- In GoHighLevel go to Automation → Workflows → Create Workflow → Start from scratch.
- Add New Trigger → Inbound Webhook. Copy the URL it shows (it starts with
https://services.leadconnectorhq.com/hooks/). - In Siteflow Pro open More → Integrations → GoHighLevel, paste the URL and pick events, for example
client.created,job.created,estimate.created,job.completedandinvoice.paid. Save. - Tap Send test in the app, then in GoHighLevel click Fetch sample request and choose the test request.
- Add a Create/Update Contact action and map
contact_first_name,contact_last_name,contact_email,contact_phoneandcontact_addressfrom the sample. - Optional: add tags, create an opportunity from
job_title,amountandpipeline_stage_label, or branch with If/Else oneventto do different things for new leads, completed jobs and payments. - 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
- Create a Zap. Trigger: Webhooks by Zapier → Catch Hook. Leave “Pick off a child key” empty.
- Copy the custom webhook URL (
https://hooks.zapier.com/hooks/catch/…). - In Siteflow Pro open More → Integrations → Zapier, paste the URL, choose events and save. Tap Send test.
- In Zapier click Test trigger and pick the request. Fields appear with names such as Data Contact Email and Data Amount.
- 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
- Create a scenario and add the Webhooks → Custom webhook module. Click Add, name it and copy the address.
- In Siteflow Pro open More → Integrations → Make, paste the address and save.
- In Make click Run once so it is listening, then tap Send test in the app. Make learns the data structure from that request.
- Add modules (HubSpot, Google Sheets, Slack, email…) and map the
data.*fields. Use a Router with filters oneventfor separate paths per event. - Turn the scenario On (scheduling set to immediately) so it keeps receiving events.
n8n
- Create a workflow and add a Webhook node. Set HTTP Method to
POSTand give it a path, for examplesiteflow. - The node shows a Test URL and a Production URL. While building, click Listen for test event and use the Test URL.
- 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 }}). - Add your nodes: HubSpot, Google Sheets, Slack, Discord, HTTP Request to any API. Use an IF or Switch node on
body.eventfor per-event logic. - 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.
- Create a sheet with headers such as Date, Customer, Email, Job, Amount, Status.
- Set up the catch step from the Zapier, Make or n8n guide above, subscribing the Siteflow webhook to events such as
client.createdorinvoice.paid. - 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. - 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)
- Build a Zapier, Make or n8n catch step as above.
- Add a Slack → Send Channel Message or Discord → Send Message action.
- 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.
// 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
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.
| Problem | What to check |
|---|---|
| Nothing arrives | Is 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 match | Hash 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 400 | They 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 test | In 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. |
| Duplicates | One 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 messages | Untick 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 records | Deletions 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_statusandinvoice_statustogether; the last payment also firesinvoice.paid. - Webhooks are outgoing only. There is no inbound API yet. See Integrations & data flow for how to combine tools.