When code is the right answer
We reach for custom code when three checks pass: the native workflow can't do it, Make or n8n can't do it cleanly, and the job is important enough to maintain for years. Typical examples from client accounts:
- A quote calculator with the business's real pricing logic, embedded in a funnel, writing the result to the contact.
- A two-way sync with industry software that has an API but no connector: practice management systems, field-service tools, property CRMs.
- Booking rules the calendar can't express: "only book a survey if the postcode is within 40 km and the job value is over a threshold".
- Document generation that merges CRM data into a branded PDF and files it back on the contact.
Four patterns that work
1. Webhook receiver
GoHighLevel workflows can send a webhook at any step. A small service receives it, does the work (look something up, calculate, call another API), and writes the result back via the GoHighLevel API. The workflow then continues based on the returned field. This handles most "we need logic the builder doesn't have" cases.
2. Embedded widget
A calculator, configurator or custom form built as a small web component and embedded in a GoHighLevel funnel page. It submits to the CRM like any form, so tagging and pipeline placement keep working.
3. Scheduled sync
A job that runs on a timer to pull or push records between GoHighLevel and another system, with batching, retries and a reconciliation report. This is how we keep a clinic's practice software and its CRM agreeing on who has an appointment tomorrow.
4. Custom form with validation
When a form needs lookups, conditional logic or file handling beyond what native forms offer, we build the form and post to GoHighLevel on submit. The visitor sees one page; the CRM sees a normal contact.
Working with the API and webhooks
A few habits keep integrations reliable. Idempotency: every webhook we receive carries an ID, and we ignore duplicates, because platforms retry. Rate limits: batch writes and back off on 429 responses instead of hammering. Field mapping in one place: a single configuration that says which custom field means what, so a renamed field doesn't break three services. And everything writes a note: when a service changes a contact, it adds a short note saying what it did and why, so the team can see the history without asking a developer.
Custom services should never hold the only copy of a customer's state. If the service disappears tomorrow, the CRM should still be complete. That single rule prevents most integration disasters.
Where the code lives
Small, single-purpose services on a managed platform: a serverless function per webhook, a scheduled job for each sync, a static-hosted widget. Each one is a few hundred lines, has its own logs, and can be replaced without touching the others. We avoid the "one custom app that does everything" approach because it becomes the thing nobody dares to change.
What we avoid
Rebuilding features GoHighLevel already has, because the platform will improve them and our copy won't. Storing customer data outside the CRM without a reason. Undocumented services that only one person understands. And code that's clever instead of clear: the person maintaining it in two years should be able to read it over a coffee.
Key takeaways
- Exhaust native features and integration tools first. Code is for the gap, not the whole flow.
- Keep GoHighLevel as the source of truth; custom services read from it and write back to it.
- Four patterns cover most needs: webhook receivers, embedded widgets, scheduled syncs and custom forms.
- Small, single-purpose services on a managed host beat one big custom app.
- Log everything a service does to the contact timeline so the team can see it without opening code.