Skip to main content

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

1

Connect the integration

In Craft, go to Company SettingsIntegrationsCustom CRM and click Connect. Give it your CRM’s name.

2

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.

3

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

FieldRequiredNotes
appointment_idYesStable ID in your CRM. Updates overwrite by this ID. Aliases: event_id, meeting_id, id
scheduled_atYesISO-8601 (with Z/offset) or epoch seconds/ms. Naive times use timezone, then the integration’s default timezone. Aliases: start_time, start
rep_emailRecommendedHow we match the appointment to a Craft rep — must equal their Craft login email. Aliases: salesperson_email, assigned_to_email
rep_idYour 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_nameRecommendedOr customer_first_name + customer_last_name. Aliases: contact_name, contact_first_name, …
customer_phone, customer_emailRecommendedUsed for search and recording matching. Aliases: contact_phone, phone, contact_email, email
address, city, state, zipRecommendedOr a single-line formatted_address. We geocode it so recordings auto-match by GPS. Aliases: project_address, project_city, …
latitude, longitudeIf your CRM already has coordinates, send them and we skip geocoding
job_idGroups multiple appointments under one job/deal. Defaults to appointment_id. Aliases: project_id, deal_id, opportunity_id
job_nameAliases: project_title, title, subject
job_statusYour CRM’s stage name, shown in Craft and usable in follow-up filters. Aliases: project_status, deal_stage
statusAppointment status. Send cancelled to stop reminders and matching for it
timezoneIANA name (e.g. America/Phoenix) for interpreting naive datetimes
lead_source, notes, sales_rep_notesExtra context, surfaced in briefings
crm_urlDeep link back to the record in your CRM
updated_atRecommendedLets 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"
}
FieldNotes
appointment_id or job_idOne required. job_id applies the result to the job’s most recent appointment that has already happened, never a future booking
dispositionCanonical 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
resultYour CRM’s outcome label, stored as-is (e.g. “Demoed, Not Sold”)
statusThe job/deal stage after this outcome
amountDeal value in dollars; $ and commas are fine
updated_atRecommended. 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

  1. Send disposition: "no_sale" — no configuration needed. In Zapier, add a Filter step so only the right stages reach the outcomes endpoint.
  2. Configure unsold results — on the integration page, list the result values (e.g. Demoed Not Sold) that should open follow-up opportunities. Then just send result and 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 attendees field like "[{'email': '...', 'role': 'SALESPERSON'}]") — if it contains an entry with a SALESPERSON-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:

  1. 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.
  2. Zap 2 — follow-up: Trigger Deal Stage ChangedFilter (only continue if the stage means “sat, didn’t buy”) → POST to your outcomes URL with disposition set to no_sale.
  3. Zap 3 — sales: Trigger Proposal Accepted → POST to your outcomes URL with disposition set to won and the proposal amount as amount.

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 timezone for 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.