If your CRM isn’t in our integrations list, you can still connect it yourself. Craft gives you two webhook endpoints; anything that can send an HTTP POST — a Zapier Zap, your CRM’s native webhooks, or a few lines of code — can feed them.
- Appointments power the AI Ride-Along: reps get recording reminders before each appointment, recordings auto-match to the right customer, and pre-appointment briefings are generated from the data you send.
- Outcomes power Follow-Up: when an appointment ends without a sale, Craft can open a follow-up opportunity and start an AI outreach campaign.
You need a Craft admin account to set this up.
Getting your endpoints
Connect the integration
In Craft, go to Company Settings → Integrations → Custom CRM and click Connect. Give it your CRM’s name.
Copy your webhook URLs
The integration page shows two URLs:
https://data.craftflow.co/api/integrations/webhook/custom/<secret>/appointments/
https://data.craftflow.co/api/integrations/webhook/custom/<secret>/outcomes/The long token in the URL is your authentication — treat these URLs like passwords.
Send a test
POST a payload to the appointments URL and watch it appear in Recent deliveries on the same page. Parsing problems show up there with a specific error message.
Sending appointments
Send one POST per appointment, whenever it’s created or updated. JSON is preferred; form-encoded bodies also work (Zapier’s default). Repeat deliveries of the same payload are deduplicated automatically, and updates simply overwrite by ID — send as often as you like.
{
"appointment_id": "appt-1042",
"job_id": "job-583",
"customer_id": "cust-291",
"scheduled_at": "2026-08-12T13:00:00Z",
"timezone": "America/Phoenix",
"rep_email": "aaron@yourcompany.com",
"rep_id": "user-77",
"customer_name": "Matt Campbell",
"customer_phone": "+17708436587",
"customer_email": "matt@example.com",
"address": "205 Oak Meadow Lane",
"city": "Woodfin",
"state": "NC",
"zip": "28804",
"job_name": "Campbell - Roofing",
"job_status": "Scheduled",
"lead_source": "Website",
"notes": "Gate code 1234",
"updated_at": "2026-08-10T15:54:19Z"
}Field reference — appointments
| Field | Required | Notes |
|---|---|---|
appointment_id | Yes | Stable ID in your CRM. Updates overwrite by this ID. Aliases: event_id, meeting_id, id |
scheduled_at | Yes | ISO-8601 (with Z/offset) or epoch seconds/ms. Naive times use timezone, then the integration’s default timezone. Aliases: start_time, start |
rep_email | Recommended | How we match the appointment to a Craft rep — must equal their Craft login email. Aliases: salesperson_email, assigned_to_email |
rep_id | Your CRM’s user ID. Once we’ve seen it alongside a matching rep_email, later payloads can send just the ID. Aliases: salesperson_id, employee_id | |
customer_name | Recommended | Or customer_first_name + customer_last_name. Aliases: contact_name, contact_first_name, … |
customer_phone, customer_email | Recommended | Used for search and recording matching. Aliases: contact_phone, phone, contact_email, email |
address, city, state, zip | Recommended | Or a single-line formatted_address. We geocode it so recordings auto-match by GPS. Aliases: project_address, project_city, … |
latitude, longitude | If your CRM already has coordinates, send them and we skip geocoding | |
job_id | Groups multiple appointments under one job/deal. Defaults to appointment_id. Aliases: project_id, deal_id, opportunity_id | |
job_name | Aliases: project_title, title, subject | |
job_status | Your CRM’s stage name, shown in Craft and usable in follow-up filters. Aliases: project_status, deal_stage | |
status | Appointment status. Send cancelled to stop reminders and matching for it | |
timezone | IANA name (e.g. America/Phoenix) for interpreting naive datetimes | |
lead_source, notes, sales_rep_notes | Extra context, surfaced in briefings | |
crm_url | Deep link back to the record in your CRM | |
updated_at | Recommended | Lets us drop stale, out-of-order deliveries |
Unknown fields are never rejected — they’re stored with the record and available to briefings.
Sending outcomes
Send a POST when an appointment gets a result or a deal closes. Identify the record with appointment_id or job_id.
{
"appointment_id": "appt-1042",
"result": "Demoed, Not Sold",
"disposition": "no_sale",
"status": "Follow Up",
"amount": "12500.50"
}| Field | Notes |
|---|---|
appointment_id or job_id | One required. job_id applies the result to the job’s most recent appointment that has already happened, never a future booking |
disposition | Canonical outcome: won, lost, no_sale, or open. no_sale opens a follow-up opportunity. won marks the job sold (with amount as the sale price) and lost marks it dead — both stop any running follow-up campaign, and neither will ever start one, even if result matches your unsold list |
result | Your CRM’s outcome label, stored as-is (e.g. “Demoed, Not Sold”) |
status | The job/deal stage after this outcome |
amount | Deal value in dollars; $ and commas are fine |
updated_at | Recommended. When this outcome happened — used as the sale date, to pick which appointment it belongs to, and to ignore older outcomes that arrive after newer ones |
Two ways to trigger follow-up
- Send
disposition: "no_sale"— no configuration needed. In Zapier, add a Filter step so only the right stages reach the outcomes endpoint. - Configure unsold results — on the integration page, list the
resultvalues (e.g.Demoed Not Sold) that should open follow-up opportunities. Then just sendresultand Craft does the matching.
Format tolerances
Built for Zapier’s quirks — all of the following parse correctly:
- Every value sent as a string (
"60","false","12500.50"). - Empty strings for unset fields (treated as missing).
- Form-encoded bodies (Zapier’s default payload type) as well as JSON.
- Nested arrays flattened to Python-style strings (e.g. an
attendeesfield like"[{'email': '...', 'role': 'SALESPERSON'}]") — if it contains an entry with aSALESPERSON-like role, we use its email for rep matching. - Epoch timestamps in seconds or milliseconds.
Recipe: DripJobs via Zapier
DripJobs has no public API, but its Zapier app covers everything this integration needs:
- Zap 1 — appointments: Trigger On-Site Estimate Scheduled → Action Webhooks by Zapier → POST to your appointments URL. Map DripJobs’ appointment ID, date/time, salesperson email, contact name/phone/email, and address to the field names above.
- Zap 2 — follow-up: Trigger Deal Stage Changed → Filter (only continue if the stage means “sat, didn’t buy”) → POST to your outcomes URL with
dispositionset tono_sale. - Zap 3 — sales: Trigger Proposal Accepted → POST to your outcomes URL with
dispositionset towonand the proposal amount asamount.
The same pattern works for any CRM with a Zapier app: one Zap per trigger, each POSTing to the matching endpoint.
Recipe: native CRM webhooks
If your CRM can send webhooks directly, point them at the endpoints and rely on the field aliases — payloads with contact_*/project_* prefixed fields, a top-level id, and start_time typically work with no changes. Send a test and check Recent deliveries; anything we couldn’t parse is spelled out there.
Delivery semantics
- Always respond fast: we accept the payload, then process asynchronously.
- 200 — accepted (repeat deliveries return
"duplicate": true). If an earlier identical delivery failed during processing, re-sending it re-runs the processing. - 422 — the payload is missing a required field (or names an unknown
timezonefor a time with no UTC offset); the response body says exactly what. Zapier surfaces this during the Zap’s test step. - 429 — rate limited (over 100 deliveries in a minute). Retry with backoff — Zapier does this automatically.
- 404 — wrong webhook URL/secret, or the integration was disconnected.
- Processing is idempotent: re-sending the full history is safe.
Need Help?
Reach out to your contact at Craft, or email support@craftflow.com.